mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 23:38:07 +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
@@ -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<WhisperServerConfiguration
|
||||
AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||
DisabledPermittedAccountAuthenticator disabledPermittedAccountAuthenticator = new DisabledPermittedAccountAuthenticator(accountsManager);
|
||||
|
||||
RateLimitResetMetricsManager rateLimitResetMetricsManager = new RateLimitResetMetricsManager(metricsCluster, Metrics.globalRegistry);
|
||||
|
||||
UnsealedSenderRateLimiter unsealedSenderRateLimiter = new UnsealedSenderRateLimiter(dynamicRateLimiters, rateLimitersCluster, dynamicConfigurationManager, rateLimitResetMetricsManager);
|
||||
PreKeyRateLimiter preKeyRateLimiter = new PreKeyRateLimiter(dynamicRateLimiters, dynamicConfigurationManager, rateLimitResetMetricsManager);
|
||||
|
||||
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerCluster, apnSender, accountsManager);
|
||||
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration(), dynamicConfigurationManager);
|
||||
SmsSender smsSender = new SmsSender(twilioSmsSender);
|
||||
@@ -512,8 +506,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
TransitionalRecaptchaClient transitionalRecaptchaClient = new TransitionalRecaptchaClient(legacyRecaptchaClient, enterpriseRecaptchaClient);
|
||||
PushChallengeManager pushChallengeManager = new PushChallengeManager(apnSender, gcmSender, pushChallengeDynamoDb);
|
||||
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||
transitionalRecaptchaClient, preKeyRateLimiter, unsealedSenderRateLimiter, dynamicRateLimiters,
|
||||
dynamicConfigurationManager);
|
||||
transitionalRecaptchaClient, dynamicRateLimiters);
|
||||
RateLimitChallengeOptionManager rateLimitChallengeOptionManager =
|
||||
new RateLimitChallengeOptionManager(dynamicRateLimiters, dynamicConfigurationManager);
|
||||
|
||||
MessagePersister messagePersister = new MessagePersister(messagesCache, messagesManager, accountsManager, dynamicConfigurationManager, Duration.ofMinutes(config.getMessageCacheConfiguration().getPersistDelayMinutes()));
|
||||
|
||||
@@ -649,7 +644,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
smsSender, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
|
||||
transitionalRecaptchaClient, gcmSender, apnSender, backupCredentialsGenerator,
|
||||
verifyExperimentEnrollmentManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager, preKeyRateLimiter, rateLimitChallengeManager));
|
||||
environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
|
||||
|
||||
final List<Object> commonControllers = Lists.newArrayList(
|
||||
new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket()),
|
||||
@@ -662,8 +657,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager,
|
||||
rateLimitChallengeManager, reportMessageManager, multiRecipientMessageExecutor),
|
||||
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, apnFallbackManager,
|
||||
reportMessageManager, multiRecipientMessageExecutor),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
@@ -705,6 +700,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
log.warn("Abusive message filter {} not annotated with @FilterAbusiveMessages and will not be installed",
|
||||
filter.getClass().getName());
|
||||
}
|
||||
|
||||
if (filter instanceof RateLimitChallengeListener) {
|
||||
log.info("Registered rate limit challenge listener: {}", filter.getClass().getName());
|
||||
rateLimitChallengeManager.addListener((RateLimitChallengeListener) filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!registeredAbusiveMessageFilter) {
|
||||
@@ -721,8 +721,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
registerCorsFilter(environment);
|
||||
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||
|
||||
RateLimitChallengeExceptionMapper rateLimitChallengeExceptionMapper = new RateLimitChallengeExceptionMapper(
|
||||
rateLimitChallengeManager);
|
||||
RateLimitChallengeExceptionMapper rateLimitChallengeExceptionMapper =
|
||||
new RateLimitChallengeExceptionMapper(rateLimitChallengeOptionManager);
|
||||
|
||||
environment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
webSocketEnvironment.jersey().register(rateLimitChallengeExceptionMapper);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.abuse;
|
||||
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface RateLimitChallengeListener {
|
||||
|
||||
void handleRateLimitChallengeAnswered(Account account);
|
||||
|
||||
/**
|
||||
* Configures this rate limit challenge listener. This method will be called before the service begins processing any
|
||||
* challenges.
|
||||
*
|
||||
* @param environmentName the name of the environment in which this listener is running (e.g. "staging" or "production")
|
||||
* @throws IOException if the listener could not read its configuration source for any reason
|
||||
*/
|
||||
void configure(String environmentName) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.abuse;
|
||||
|
||||
public enum RateLimitChallengeType {
|
||||
|
||||
PUSH_CHALLENGE,
|
||||
RECAPTCHA
|
||||
}
|
||||
@@ -11,9 +11,6 @@ import javax.validation.constraints.NotNull;
|
||||
|
||||
public class DynamicRateLimitChallengeConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
private boolean preKeyLimitEnforced = false;
|
||||
|
||||
@JsonProperty
|
||||
boolean unsealedSenderLimitEnforced = false;
|
||||
|
||||
@@ -30,10 +27,6 @@ public class DynamicRateLimitChallengeConfiguration {
|
||||
return Optional.ofNullable(clientSupportedVersions.get(platform));
|
||||
}
|
||||
|
||||
public boolean isPreKeyLimitEnforced() {
|
||||
return preKeyLimitEnforced;
|
||||
}
|
||||
|
||||
public boolean isUnsealedSenderLimitEnforced() {
|
||||
return unsealedSenderLimitEnforced;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@ public class DynamicRateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private int unsealedSenderPermitIncrement = 50;
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration unsealedSenderIp = new RateLimitConfiguration(120, 2.0 / 60);
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration rateLimitReset = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@@ -34,13 +31,6 @@ public class DynamicRateLimitsConfiguration {
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration pushChallengeSuccess = new RateLimitConfiguration(2, 2.0 / (60 * 24));
|
||||
|
||||
@JsonProperty
|
||||
private RateLimitConfiguration dailyPreKeys = new RateLimitConfiguration(50, 50.0 / (24.0 * 60));
|
||||
|
||||
public RateLimitConfiguration getUnsealedSenderIp() {
|
||||
return unsealedSenderIp;
|
||||
}
|
||||
|
||||
public CardinalityRateLimitConfiguration getUnsealedSenderNumber() {
|
||||
return unsealedSenderNumber;
|
||||
}
|
||||
@@ -72,8 +62,4 @@ public class DynamicRateLimitsConfiguration {
|
||||
public int getUnsealedSenderPermitIncrement() {
|
||||
return unsealedSenderPermitIncrement;
|
||||
}
|
||||
|
||||
public RateLimitConfiguration getDailyPreKeys() {
|
||||
return dailyPreKeys;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.limits.PreKeyRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
@@ -57,24 +54,16 @@ public class KeysController {
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final PreKeyRateLimiter preKeyRateLimiter;
|
||||
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
|
||||
private static final String PREKEY_REQUEST_COUNTER_NAME = name(KeysController.class, "preKeyGet");
|
||||
private static final String RATE_LIMITED_GET_PREKEYS_COUNTER_NAME = name(KeysController.class, "rateLimitedGetPreKeys");
|
||||
|
||||
private static final String SOURCE_COUNTRY_TAG_NAME = "sourceCountry";
|
||||
private static final String INTERNATIONAL_TAG_NAME = "international";
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||
PreKeyRateLimiter preKeyRateLimiter,
|
||||
RateLimitChallengeManager rateLimitChallengeManager) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.keys = keys;
|
||||
this.accounts = accounts;
|
||||
this.preKeyRateLimiter = preKeyRateLimiter;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
this.accounts = accounts;
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -142,7 +131,7 @@ public class KeysController {
|
||||
@PathParam("identifier") UUID targetUuid,
|
||||
@PathParam("device_id") String deviceId,
|
||||
@HeaderParam("User-Agent") String userAgent)
|
||||
throws RateLimitExceededException, RateLimitChallengeException, ServerRejectedException {
|
||||
throws RateLimitExceededException {
|
||||
|
||||
if (!auth.isPresent() && !accessKey.isPresent()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
@@ -174,23 +163,6 @@ public class KeysController {
|
||||
rateLimiters.getPreKeysLimiter().validate(
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetUuid
|
||||
+ "." + deviceId);
|
||||
|
||||
try {
|
||||
preKeyRateLimiter.validate(account.get());
|
||||
} catch (RateLimitExceededException e) {
|
||||
|
||||
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
|
||||
|
||||
Metrics.counter(RATE_LIMITED_GET_PREKEYS_COUNTER_NAME,
|
||||
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
|
||||
"legacyClient", String.valueOf(legacyClient))
|
||||
.increment();
|
||||
|
||||
if (legacyClient) {
|
||||
throw new ServerRejectedException();
|
||||
}
|
||||
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
|
||||
final boolean usePhoneNumberIdentity = target.getPhoneNumberIdentifier().equals(targetUuid);
|
||||
|
||||
@@ -75,9 +75,7 @@ import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.UnsealedSenderRateLimiter;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
|
||||
@@ -109,9 +107,7 @@ public class MessageController {
|
||||
private final ReceiptSender receiptSender;
|
||||
private final AccountsManager accountsManager;
|
||||
private final MessagesManager messagesManager;
|
||||
private final UnsealedSenderRateLimiter unsealedSenderRateLimiter;
|
||||
private final ApnFallbackManager apnFallbackManager;
|
||||
private final RateLimitChallengeManager rateLimitChallengeManager;
|
||||
private final ReportMessageManager reportMessageManager;
|
||||
private final ExecutorService multiRecipientMessageExecutor;
|
||||
|
||||
@@ -145,9 +141,7 @@ public class MessageController {
|
||||
ReceiptSender receiptSender,
|
||||
AccountsManager accountsManager,
|
||||
MessagesManager messagesManager,
|
||||
UnsealedSenderRateLimiter unsealedSenderRateLimiter,
|
||||
ApnFallbackManager apnFallbackManager,
|
||||
RateLimitChallengeManager rateLimitChallengeManager,
|
||||
ReportMessageManager reportMessageManager,
|
||||
@Nonnull ExecutorService multiRecipientMessageExecutor) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
@@ -155,9 +149,7 @@ public class MessageController {
|
||||
this.receiptSender = receiptSender;
|
||||
this.accountsManager = accountsManager;
|
||||
this.messagesManager = messagesManager;
|
||||
this.unsealedSenderRateLimiter = unsealedSenderRateLimiter;
|
||||
this.apnFallbackManager = apnFallbackManager;
|
||||
this.rateLimitChallengeManager = rateLimitChallengeManager;
|
||||
this.reportMessageManager = reportMessageManager;
|
||||
this.multiRecipientMessageExecutor = Objects.requireNonNull(multiRecipientMessageExecutor);
|
||||
}
|
||||
@@ -238,24 +230,6 @@ public class MessageController {
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
unsealedSenderRateLimiter.validate(source.get().getAccount(), destination.get());
|
||||
} catch (final RateLimitExceededException e) {
|
||||
|
||||
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
|
||||
final String rateLimitReason = legacyClient ? "unsealedSenderCardinality" : "challengeIssued";
|
||||
|
||||
Metrics.counter(RATE_LIMITED_MESSAGE_COUNTER_NAME,
|
||||
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
|
||||
RATE_LIMIT_REASON_TAG_NAME, rateLimitReason).increment();
|
||||
|
||||
if (legacyClient) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new RateLimitChallengeException(source.get().getAccount(), e.getRetryDuration());
|
||||
}
|
||||
}
|
||||
|
||||
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage,
|
||||
|
||||
@@ -5,27 +5,23 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.limits;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.CardinalityRateLimitConfiguration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration.RateLimitConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class DynamicRateLimiters {
|
||||
|
||||
private final FaultTolerantRedisCluster cacheCluster;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
private final AtomicReference<CardinalityRateLimiter> unsealedSenderCardinalityLimiter;
|
||||
private final AtomicReference<RateLimiter> unsealedIpLimiter;
|
||||
private final AtomicReference<RateLimiter> rateLimitResetLimiter;
|
||||
private final AtomicReference<RateLimiter> recaptchaChallengeAttemptLimiter;
|
||||
private final AtomicReference<RateLimiter> recaptchaChallengeSuccessLimiter;
|
||||
private final AtomicReference<RateLimiter> pushChallengeAttemptLimiter;
|
||||
private final AtomicReference<RateLimiter> pushChallengeSuccessLimiter;
|
||||
private final AtomicReference<RateLimiter> dailyPreKeysLimiter;
|
||||
|
||||
public DynamicRateLimiters(final FaultTolerantRedisCluster rateLimitCluster,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> 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> rateLimiter,
|
||||
RateLimitConfiguration currentConfiguration,
|
||||
BiFunction<FaultTolerantRedisCluster, RateLimitConfiguration, RateLimiter> 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,
|
||||
|
||||
@@ -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<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final RateLimitResetMetricsManager metricsManager;
|
||||
|
||||
public PreKeyRateLimiter(final DynamicRateLimiters rateLimiters,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
public static final String OPTION_RECAPTCHA = "recaptcha";
|
||||
public static final String OPTION_PUSH_CHALLENGE = "pushChallenge";
|
||||
private final List<RateLimitChallengeListener> 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<DynamicConfiguration> 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<Semver> 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<String> getChallengeOptions(final Account account) {
|
||||
final List<String> 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 {
|
||||
|
||||
@@ -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<DynamicConfiguration> dynamicConfigurationManager;
|
||||
|
||||
public static final String OPTION_RECAPTCHA = "recaptcha";
|
||||
public static final String OPTION_PUSH_CHALLENGE = "pushChallenge";
|
||||
|
||||
public RateLimitChallengeOptionManager(final DynamicRateLimiters rateLimiters,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
}
|
||||
|
||||
public boolean isClientBelowMinimumVersion(final String userAgent) {
|
||||
try {
|
||||
final UserAgent client = UserAgentUtil.parseUserAgentString(userAgent);
|
||||
final Optional<Semver> 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<String> getChallengeOptions(final Account account) {
|
||||
final List<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.<Long>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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<DynamicConfiguration> 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<DynamicConfiguration> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<RateLimitChallengeException> {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user