Send 508 status code for legacy clients that produce rate limit challenges

This commit is contained in:
Chris Eager
2021-08-10 10:10:59 -05:00
committed by Chris Eager
parent d29764d11f
commit b3e6a50dee
11 changed files with 126 additions and 66 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm;
@@ -110,6 +110,7 @@ import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressException
import org.whispersystems.textsecuregcm.mappers.RateLimitChallengeExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.RetryLaterExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
import org.whispersystems.textsecuregcm.metrics.BufferPoolGauges;
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
import org.whispersystems.textsecuregcm.metrics.FileDescriptorGauge;
@@ -662,6 +663,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
environment.jersey().register(new RetryLaterExceptionMapper());
environment.jersey().register(new ServerRejectedExceptionMapper());
webSocketEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
webSocketEnvironment.jersey().register(new IOExceptionMapper());
@@ -669,6 +671,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
webSocketEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
webSocketEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
webSocketEnvironment.jersey().register(new RetryLaterExceptionMapper());
webSocketEnvironment.jersey().register(new ServerRejectedExceptionMapper());
provisioningEnvironment.jersey().register(new LoggingUnhandledExceptionMapper());
provisioningEnvironment.jersey().register(new IOExceptionMapper());
@@ -676,6 +679,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
provisioningEnvironment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
provisioningEnvironment.jersey().register(new DeviceLimitExceededExceptionMapper());
provisioningEnvironment.jersey().register(new RetryLaterExceptionMapper());
provisioningEnvironment.jersey().register(new ServerRejectedExceptionMapper());
}
private void registerCorsFilter(Environment environment) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@@ -116,12 +116,12 @@ public class KeysController {
@GET
@Path("/{identifier}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getDeviceKeys(@Auth Optional<Account> account,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("identifier") AmbiguousIdentifier targetName,
@PathParam("device_id") String deviceId,
@HeaderParam("User-Agent") String userAgent)
throws RateLimitExceededException, RateLimitChallengeException {
public Response getDeviceKeys(@Auth Optional<Account> account,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("identifier") AmbiguousIdentifier targetName,
@PathParam("device_id") String deviceId,
@HeaderParam("User-Agent") String userAgent)
throws RateLimitExceededException, RateLimitChallengeException, ServerRejectedException {
targetName.incrementRequestCounter("getDeviceKeys", userAgent);
@@ -152,16 +152,17 @@ public class KeysController {
preKeyRateLimiter.validate(account.get());
} catch (RateLimitExceededException e) {
final boolean enforceLimit = rateLimitChallengeManager.shouldIssueRateLimitChallenge(userAgent);
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
Metrics.counter(RATE_LIMITED_GET_PREKEYS_COUNTER_NAME,
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
"enforced", String.valueOf(enforceLimit))
SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.get().getNumber()),
"legacyClient", String.valueOf(legacyClient))
.increment();
if (enforceLimit) {
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
if (legacyClient) {
throw new ServerRejectedException();
}
throw new RateLimitChallengeException(account.get(), e.getRetryDuration());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
@@ -19,6 +19,7 @@ import io.dropwizard.util.DataSize;
import io.lettuce.core.ScriptOutputType;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import java.io.IOException;
import java.security.MessageDigest;
import java.time.Duration;
@@ -56,7 +57,6 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import io.micrometer.core.instrument.Tags;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -265,20 +265,18 @@ public class MessageController {
unsealedSenderRateLimiter.validate(source.get(), destination.get());
} catch (final RateLimitExceededException e) {
final boolean enforceLimit = rateLimitChallengeManager.shouldIssueRateLimitChallenge(userAgent);
final boolean legacyClient = rateLimitChallengeManager.isClientBelowMinimumVersion(userAgent);
Metrics.counter(REJECT_UNSEALED_SENDER_COUNTER_NAME,
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
"enforced", String.valueOf(enforceLimit))
SENDER_COUNTRY_TAG_NAME, senderCountryCode,
"legacyClient", String.valueOf(legacyClient))
.increment();
if (enforceLimit) {
logger.debug("Rejected unsealed sender limit from: {}", source.get().getNumber());
throw new RateLimitChallengeException(source.get(), e.getRetryDuration());
} else {
if (legacyClient) {
throw e;
}
throw new RateLimitChallengeException(source.get(), e.getRetryDuration());
}
final String destinationCountryCode = Util.getCountryCode(destination.get().getNumber());

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
public class ServerRejectedException extends Exception {
}

View File

@@ -1,7 +1,12 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.limits;
import org.whispersystems.textsecuregcm.storage.Account;
import java.time.Duration;
import org.whispersystems.textsecuregcm.storage.Account;
public class RateLimitChallengeException extends Exception {
@@ -20,4 +25,5 @@ public class RateLimitChallengeException extends Exception {
public Duration getRetryAfter() {
return retryAfter;
}
}

View File

@@ -95,15 +95,15 @@ public class RateLimitChallengeManager {
unsealedSenderRateLimiter.handleRateLimitReset(account);
}
public boolean shouldIssueRateLimitChallenge(final String userAgent) {
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.isLowerThanOrEqualTo(client.getVersion()))
.orElse(false);
return minimumClientVersion.map(version -> version.isGreaterThan(client.getVersion()))
.orElse(true);
} catch (final UnrecognizedUserAgentException ignored) {
return false;
}

View File

@@ -1,11 +1,16 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.mappers;
import org.whispersystems.textsecuregcm.entities.RateLimitChallenge;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
import java.util.UUID;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import java.util.UUID;
import org.whispersystems.textsecuregcm.entities.RateLimitChallenge;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeException;
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
public class RateLimitChallengeExceptionMapper implements ExceptionMapper<RateLimitChallengeException> {
@@ -18,8 +23,10 @@ public class RateLimitChallengeExceptionMapper implements ExceptionMapper<RateLi
@Override
public Response toResponse(final RateLimitChallengeException exception) {
return Response.status(428)
.entity(new RateLimitChallenge(UUID.randomUUID().toString(), rateLimitChallengeManager.getChallengeOptions(exception.getAccount())))
.entity(new RateLimitChallenge(UUID.randomUUID().toString(),
rateLimitChallengeManager.getChallengeOptions(exception.getAccount())))
.header("Retry-After", exception.getRetryAfter().toSeconds())
.build();
}
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.mappers;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import org.whispersystems.textsecuregcm.controllers.ServerRejectedException;
public class ServerRejectedExceptionMapper implements ExceptionMapper<ServerRejectedException> {
@Override
public Response toResponse(final ServerRejectedException exception) {
return Response.status(508).build();
}
}