mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Handle 428 rate limiting.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user