mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 05:58:05 +01:00
Return a Retry-After on rate-limited responses
Previously, only endpoints throwing a RetryLaterException would include a Retry-After header in the 413 response. Now, by default, all RateLimitExceededExceptions will be marshalled into a 413 with a Retry-After included if possible.
This commit is contained in:
committed by
ravi-signal
parent
43792e2426
commit
ae3a5c5f5e
@@ -123,7 +123,6 @@ import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressException
|
||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||
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.ApplicationShutdownMonitor;
|
||||
import org.whispersystems.textsecuregcm.metrics.BufferPoolGauges;
|
||||
@@ -758,7 +757,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new RateLimitExceededExceptionMapper(),
|
||||
new InvalidWebsocketAddressExceptionMapper(),
|
||||
new DeviceLimitExceededExceptionMapper(),
|
||||
new RetryLaterExceptionMapper(),
|
||||
new ServerRejectedExceptionMapper(),
|
||||
new ImpossiblePhoneNumberExceptionMapper(),
|
||||
new NonNormalizedPhoneNumberExceptionMapper()
|
||||
|
||||
@@ -213,7 +213,7 @@ public class AccountController {
|
||||
@QueryParam("client") Optional<String> client,
|
||||
@QueryParam("captcha") Optional<String> captcha,
|
||||
@QueryParam("challenge") Optional<String> pushChallenge)
|
||||
throws RateLimitExceededException, RetryLaterException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||
throws RateLimitExceededException, ImpossiblePhoneNumberException, NonNormalizedPhoneNumberException {
|
||||
|
||||
Util.requireNormalizedNumber(number);
|
||||
|
||||
@@ -234,24 +234,16 @@ public class AccountController {
|
||||
return Response.status(402).build();
|
||||
}
|
||||
|
||||
try {
|
||||
switch (transport) {
|
||||
case "sms":
|
||||
rateLimiters.getSmsDestinationLimiter().validate(number);
|
||||
break;
|
||||
case "voice":
|
||||
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
||||
rateLimiters.getVoiceDestinationDailyLimiter().validate(number);
|
||||
break;
|
||||
default:
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
} catch (RateLimitExceededException e) {
|
||||
if (!e.getRetryDuration().isNegative()) {
|
||||
throw new RetryLaterException(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
switch (transport) {
|
||||
case "sms":
|
||||
rateLimiters.getSmsDestinationLimiter().validate(number);
|
||||
break;
|
||||
case "voice":
|
||||
rateLimiters.getVoiceDestinationLimiter().validate(number);
|
||||
rateLimiters.getVoiceDestinationDailyLimiter().validate(number);
|
||||
break;
|
||||
default:
|
||||
throw new WebApplicationException(Response.status(422).build());
|
||||
}
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode(number);
|
||||
@@ -643,7 +635,9 @@ public class AccountController {
|
||||
}
|
||||
|
||||
final String mostRecentProxy = ForwardedIpUtil.getMostRecentProxy(forwardedFor)
|
||||
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(1)));
|
||||
// Missing/malformed Forwarded-For, cannot calculate a reasonable backoff
|
||||
// duration
|
||||
.orElseThrow(() -> new RateLimitExceededException(Duration.ofHours(-1)));
|
||||
|
||||
rateLimiters.getCheckAccountExistenceLimiter().validate(mostRecentProxy);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ChallengeController {
|
||||
public Response handleChallengeResponse(@Auth final AuthenticatedAccount auth,
|
||||
@Valid final AnswerChallengeRequest answerRequest,
|
||||
@HeaderParam("X-Forwarded-For") final String forwardedFor,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws RetryLaterException {
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent) throws RateLimitExceededException {
|
||||
|
||||
Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent));
|
||||
|
||||
@@ -76,8 +76,6 @@ public class ChallengeController {
|
||||
} else {
|
||||
tags = tags.and(CHALLENGE_TYPE_TAG, "unrecognized");
|
||||
}
|
||||
} catch (final RateLimitExceededException e) {
|
||||
throw new RetryLaterException(e);
|
||||
} finally {
|
||||
Metrics.counter(CHALLENGE_RESPONSE_COUNTER_NAME, tags).increment();
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
public class RateLimitExceededException extends Exception {
|
||||
|
||||
private final Duration retryDuration;
|
||||
private final Optional<Duration> retryDuration;
|
||||
|
||||
public RateLimitExceededException(final Duration retryDuration) {
|
||||
this(null, retryDuration);
|
||||
@@ -16,8 +17,9 @@ public class RateLimitExceededException extends Exception {
|
||||
|
||||
public RateLimitExceededException(final String message, final Duration retryDuration) {
|
||||
super(message, null, true, false);
|
||||
this.retryDuration = retryDuration;
|
||||
// we won't provide a backoff in the case the duration is negative
|
||||
this.retryDuration = retryDuration.isNegative() ? Optional.empty() : Optional.of(retryDuration);
|
||||
}
|
||||
|
||||
public Duration getRetryDuration() { return retryDuration; }
|
||||
public Optional<Duration> getRetryDuration() { return retryDuration; }
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class RetryLaterException extends Exception {
|
||||
private final Duration backoffDuration;
|
||||
|
||||
public RetryLaterException(RateLimitExceededException e) {
|
||||
super(null, e, true, false);
|
||||
this.backoffDuration = e.getRetryDuration();
|
||||
}
|
||||
|
||||
public Duration getBackoffDuration() { return backoffDuration; }
|
||||
}
|
||||
@@ -12,8 +12,18 @@ import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class RateLimitExceededExceptionMapper implements ExceptionMapper<RateLimitExceededException> {
|
||||
|
||||
/**
|
||||
* Convert a RateLimitExceededException to a 413 response with a
|
||||
* Retry-After header.
|
||||
*
|
||||
* @param e A RateLimitExceededException potentially containing a reccomended retry duration
|
||||
* @return the response
|
||||
*/
|
||||
@Override
|
||||
public Response toResponse(RateLimitExceededException e) {
|
||||
return Response.status(413).build();
|
||||
return e.getRetryDuration()
|
||||
.map(d -> Response.status(413).header("Retry-After", d.toSeconds()))
|
||||
.orElseGet(() -> Response.status(413)).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.mappers;
|
||||
|
||||
import org.whispersystems.textsecuregcm.controllers.RetryLaterException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class RetryLaterExceptionMapper implements ExceptionMapper<RetryLaterException> {
|
||||
@Override
|
||||
public Response toResponse(RetryLaterException e) {
|
||||
return Response.status(413)
|
||||
.header("Retry-After", e.getBackoffDuration().toSeconds())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user