Handle 428 rate limiting.

This commit is contained in:
Greyson Parrelli
2021-05-05 12:49:18 -04:00
parent 02d060ca0a
commit 31e1c6f7aa
60 changed files with 1235 additions and 57 deletions

View File

@@ -198,7 +198,7 @@ public class SignalServiceAccountManager {
* @param e164number The number to associate it with.
* @throws IOException
*/
public void requestPushChallenge(String gcmRegistrationId, String e164number) throws IOException {
public void requestRegistrationPushChallenge(String gcmRegistrationId, String e164number) throws IOException {
this.pushServiceSocket.requestPushChallenge(gcmRegistrationId, e164number);
}
@@ -711,6 +711,18 @@ public class SignalServiceAccountManager {
this.pushServiceSocket.deleteAccount();
}
public void requestRateLimitPushChallenge() throws IOException {
this.pushServiceSocket.requestRateLimitPushChallenge();
}
public void submitRateLimitPushChallenge(String challenge) throws IOException {
this.pushServiceSocket.submitRateLimitPushChallenge(challenge);
}
public void submitRateLimitRecaptchaChallenge(String challenge, String recaptchaToken) throws IOException {
this.pushServiceSocket.submitRateLimitRecaptchaChallenge(challenge, recaptchaToken);
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
this.pushServiceSocket.setSoTimeoutMillis(soTimeoutMillis);
}

View File

@@ -8,7 +8,6 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
@@ -26,12 +25,14 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
@@ -196,6 +197,12 @@ public class SignalServiceMessagePipe {
return FutureTransformers.map(response, value -> {
if (value.getStatus() == 404) {
throw new UnregisteredUserException(list.getDestination(), new NotFoundException("not found"));
} else if (value.getStatus() == 428) {
ProofRequiredResponse proofResponse = JsonUtil.fromJson(value.getBody(), ProofRequiredResponse.class);
String retryAfterRaw = value.getHeader("Retry-After");
long retryAfter = Util.parseInt(retryAfterRaw, -1);
throw new ProofRequiredException(proofResponse, retryAfter);
} else if (value.getStatus() == 508) {
throw new ServerRejectedException();
} else if (value.getStatus() < 200 || value.getStatus() >= 300) {

View File

@@ -55,6 +55,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
@@ -68,6 +69,7 @@ import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttribut
import org.whispersystems.signalservice.internal.push.MismatchedDevices;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
import org.whispersystems.signalservice.internal.push.ProvisioningProtos;
import org.whispersystems.signalservice.internal.push.PushAttachmentData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
@@ -98,6 +100,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
@@ -1424,6 +1427,9 @@ public class SignalServiceMessageSender {
} else if (e.getCause() instanceof ServerRejectedException) {
Log.w(TAG, e);
throw ((ServerRejectedException) e.getCause());
} else if (e.getCause() instanceof ProofRequiredException) {
Log.w(TAG, e);
results.add(SendMessageResult.proofRequiredFailure(recipient, (ProofRequiredException) e.getCause()));
} else {
throw new IOException(e);
}

View File

@@ -3,29 +3,35 @@ package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
public class SendMessageResult {
private final SignalServiceAddress address;
private final Success success;
private final boolean networkFailure;
private final boolean unregisteredFailure;
private final IdentityFailure identityFailure;
private final SignalServiceAddress address;
private final Success success;
private final boolean networkFailure;
private final boolean unregisteredFailure;
private final IdentityFailure identityFailure;
private final ProofRequiredException proofRequiredFailure;
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync, long duration) {
return new SendMessageResult(address, new Success(unidentified, needsSync, duration), false, false, null);
return new SendMessageResult(address, new Success(unidentified, needsSync, duration), false, false, null, null);
}
public static SendMessageResult networkFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, true, false, null);
return new SendMessageResult(address, null, true, false, null, null);
}
public static SendMessageResult unregisteredFailure(SignalServiceAddress address) {
return new SendMessageResult(address, null, false, true, null);
return new SendMessageResult(address, null, false, true, null, null);
}
public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) {
return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey));
return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey), null);
}
public static SendMessageResult proofRequiredFailure(SignalServiceAddress address, ProofRequiredException proofRequiredException) {
return new SendMessageResult(address, null, false, false, null, proofRequiredException);
}
public SignalServiceAddress getAddress() {
@@ -37,7 +43,7 @@ public class SendMessageResult {
}
public boolean isNetworkFailure() {
return networkFailure;
return networkFailure || proofRequiredFailure != null;
}
public boolean isUnregisteredFailure() {
@@ -48,12 +54,23 @@ public class SendMessageResult {
return identityFailure;
}
private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure) {
public ProofRequiredException getProofRequiredFailure() {
return proofRequiredFailure;
}
private SendMessageResult(SignalServiceAddress address,
Success success,
boolean networkFailure,
boolean unregisteredFailure,
IdentityFailure identityFailure,
ProofRequiredException proofRequiredFailure)
{
this.address = address;
this.success = success;
this.networkFailure = networkFailure;
this.unregisteredFailure = unregisteredFailure;
this.identityFailure = identityFailure;
this.proofRequiredFailure = proofRequiredFailure;
}
public static class Success {

View File

@@ -0,0 +1,63 @@
package org.whispersystems.signalservice.api.push.exceptions;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Thrown when rate-limited by the server and proof of humanity is required to continue messaging.
*/
public class ProofRequiredException extends NonSuccessfulResponseCodeException {
private static final String TAG = "ProofRequiredRateLimit";
private final String token;
private final Set<Option> options;
private final long retryAfterSeconds;
public ProofRequiredException(ProofRequiredResponse response, long retryAfterSeconds) {
super(428);
this.token = response.getToken();
this.options = parseOptions(response.getOptions());
this.retryAfterSeconds = retryAfterSeconds;
}
public String getToken() {
return token;
}
public Set<Option> getOptions() {
return options;
}
public long getRetryAfterSeconds() {
return retryAfterSeconds;
}
private static Set<Option> parseOptions(List<String> rawOptions) {
Set<Option> options = new HashSet<>(rawOptions.size());
for (String raw : rawOptions) {
switch (raw) {
case "recaptcha":
options.add(Option.RECAPTCHA);
break;
case "pushChallenge":
options.add(Option.PUSH_CHALLENGE);
break;
default:
Log.w(TAG, "Unrecognized challenge option: " + raw);
break;
}
}
return options;
}
public enum Option {
RECAPTCHA, PUSH_CHALLENGE
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class ProofRequiredResponse {
@JsonProperty
public String token;
@JsonProperty
public List<String> options;
public ProofRequiredResponse() {}
public String getToken() {
return token;
}
public List<String> getOptions() {
return options;
}
}

View File

@@ -60,6 +60,7 @@ import org.whispersystems.signalservice.api.push.exceptions.MissingConfiguration
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.push.exceptions.RangeException;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
@@ -221,6 +222,9 @@ public class PushServiceSocket {
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
private static final String SUBMIT_RATE_LIMIT_CHALLENGE = "/v1/challenge";
private static final String REQUEST_RATE_LIMIT_PUSH_CHALLENGE = "/v1/challenge/push";
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
@@ -770,6 +774,20 @@ public class PushServiceSocket {
makeServiceRequest(DELETE_ACCOUNT_PATH, "DELETE", null);
}
public void requestRateLimitPushChallenge() throws IOException {
makeServiceRequest(REQUEST_RATE_LIMIT_PUSH_CHALLENGE, "POST", "");
}
public void submitRateLimitPushChallenge(String challenge) throws IOException {
String payload = JsonUtil.toJson(new SubmitPushChallengePayload(challenge));
makeServiceRequest(SUBMIT_RATE_LIMIT_CHALLENGE, "PUT", payload);
}
public void submitRateLimitRecaptchaChallenge(String challenge, String recaptchaToken) throws IOException {
String payload = JsonUtil.toJson(new SubmitRecaptchaChallengePayload(challenge, recaptchaToken));
makeServiceRequest(SUBMIT_RATE_LIMIT_CHALLENGE, "PUT", payload);
}
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
@@ -1468,6 +1486,13 @@ public class PushServiceSocket {
throw new LockedException(accountLockFailure.length,
accountLockFailure.timeRemaining,
basicStorageCredentials);
case 428:
ProofRequiredResponse proofRequiredResponse = readResponseJson(response, ProofRequiredResponse.class);
String retryAfterRaw = response.header("Retry-After");
long retryAfter = Util.parseInt(retryAfterRaw, -1);
throw new ProofRequiredException(proofRequiredResponse, retryAfter);
case 499:
throw new DeprecatedVersionException();

View File

@@ -0,0 +1,19 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
class SubmitPushChallengePayload {
@JsonProperty
private String type;
@JsonProperty
private String challenge;
public SubmitPushChallengePayload() {}
public SubmitPushChallengePayload(String challenge) {
this.type = "rateLimitPushChallenge";
this.challenge = challenge;
}
}

View File

@@ -0,0 +1,23 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
class SubmitRecaptchaChallengePayload {
@JsonProperty
private String type;
@JsonProperty
private String token;
@JsonProperty
private String captcha;
public SubmitRecaptchaChallengePayload() {}
public SubmitRecaptchaChallengePayload(String challenge, String recaptchaToken) {
this.type = "recaptcha";
this.token = challenge;
this.captcha = recaptchaToken;
}
}

View File

@@ -150,4 +150,11 @@ public class Util {
return Collections.unmodifiableList(Arrays.asList(elements.clone()));
}
public static int parseInt(String integer, int defaultValue) {
try {
return Integer.parseInt(integer);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}

View File

@@ -255,7 +255,8 @@ public class WebSocketConnection extends WebSocketListener {
OutgoingRequest listener = outgoingRequests.get(message.getResponse().getId());
if (listener != null) {
listener.getResponseFuture().set(new WebsocketResponse(message.getResponse().getStatus(),
new String(message.getResponse().getBody().toByteArray())));
new String(message.getResponse().getBody().toByteArray()),
message.getResponse().getHeadersList()));
}
}

View File

@@ -1,12 +1,20 @@
package org.whispersystems.signalservice.internal.websocket;
public class WebsocketResponse {
private final int status;
private final String body;
import org.whispersystems.libsignal.util.guava.Preconditions;
WebsocketResponse(int status, String body) {
this.status = status;
this.body = body;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WebsocketResponse {
private final int status;
private final String body;
private final Map<String, String> headers;
WebsocketResponse(int status, String body, List<String> headers) {
this.status = status;
this.body = body;
this.headers = parseHeaders(headers);
}
public int getStatus() {
@@ -16,4 +24,27 @@ public class WebsocketResponse {
public String getBody() {
return body;
}
public String getHeader(String key) {
return headers.get(Preconditions.checkNotNull(key.toLowerCase()));
}
private static Map<String, String> parseHeaders(List<String> rawHeaders) {
Map<String, String> headers = new HashMap<>(rawHeaders.size());
for (String raw : rawHeaders) {
if (raw != null && raw.length() > 0) {
int colonIndex = raw.indexOf(":");
if (colonIndex > 0 && colonIndex < raw.length() - 1) {
String key = raw.substring(0, colonIndex).trim().toLowerCase();
String value = raw.substring(colonIndex + 1).trim();
headers.put(key, value);
}
}
}
return headers;
}
}