diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java index d72b36ba2..1a281e939 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcService.java @@ -27,6 +27,7 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidVersionException; import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.MultiRecipientMismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.MessageProtos; @@ -37,7 +38,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.push.MessageSender; import org.whispersystems.textsecuregcm.push.MessageTooLargeException; import org.whispersystems.textsecuregcm.push.MessageUtil; -import org.whispersystems.textsecuregcm.spam.GrpcResponse; +import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse; import org.whispersystems.textsecuregcm.spam.MessageType; import org.whispersystems.textsecuregcm.spam.SpamCheckResult; import org.whispersystems.textsecuregcm.spam.SpamChecker; @@ -155,16 +156,15 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me final boolean urgent, final boolean story) throws RateLimitExceededException { - final SpamCheckResult> spamCheckResult = + final SpamCheckResult spamCheckResult = spamChecker.checkForIndividualRecipientSpamGrpc( story ? MessageType.INDIVIDUAL_STORY : MessageType.INDIVIDUAL_SEALED_SENDER, Optional.empty(), Optional.of(destination), destinationServiceIdentifier); - if (spamCheckResult.response().isPresent()) { - return spamCheckResult.response().get().getResponseOrThrowStatus(); - } + spamCheckResult.response().ifPresent(grpcResponse -> + grpcResponse.throwStatusOr(_ -> GrpcExceptions.rateLimitExceeded(null))); try { final int totalPayloadLength = messages.getMessagesMap().values().stream() @@ -208,12 +208,22 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me entry -> entry.getKey().byteValue(), entry -> entry.getValue().getRegistrationId())); - return MessagesGrpcHelper.sendMessage(messageSender, - destination, - destinationServiceIdentifier, - messagesByDeviceId, - registrationIdsByDeviceId, - Optional.empty()); + try { + messageSender.sendMessages(destination, + destinationServiceIdentifier, + messagesByDeviceId, + registrationIdsByDeviceId, + Optional.empty(), + RequestAttributesUtil.getUserAgent().orElse(null)); + + return SEND_MESSAGE_SUCCESS_RESPONSE; + } catch (final MismatchedDevicesException e) { + return SendMessageResponse.newBuilder() + .setMismatchedDevices(MessagesGrpcHelper.buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices())) + .build(); + } catch (final MessageTooLargeException e) { + throw GrpcExceptions.invalidArguments("message too large"); + } } @Override @@ -265,14 +275,13 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me final boolean urgent, final boolean story) { - final SpamCheckResult> spamCheckResult = + final SpamCheckResult spamCheckResult = spamChecker.checkForMultiRecipientSpamGrpc(story ? MessageType.MULTI_RECIPIENT_STORY : MessageType.MULTI_RECIPIENT_SEALED_SENDER); - if (spamCheckResult.response().isPresent()) { - return spamCheckResult.response().get().getResponseOrThrowStatus(); - } + spamCheckResult.response().ifPresent(response -> + response.throwStatusOr(_ -> GrpcExceptions.rateLimitExceeded(null))); // At this point, the caller has at least superficially provided the information needed to send a multi-recipient // message. Attempt to resolve the destination service identifiers to Signal accounts. diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java index 5e753fce3..424469213 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcHelper.java @@ -5,71 +5,11 @@ package org.whispersystems.textsecuregcm.grpc; -import com.google.protobuf.Empty; -import io.grpc.Status; -import io.grpc.StatusException; -import java.util.Map; -import java.util.Optional; import org.signal.chat.messages.MismatchedDevices; -import org.signal.chat.messages.SendMessageResponse; -import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; -import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; -import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; -import org.whispersystems.textsecuregcm.push.MessageSender; -import org.whispersystems.textsecuregcm.push.MessageTooLargeException; -import org.whispersystems.textsecuregcm.storage.Account; public class MessagesGrpcHelper { - private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse - .newBuilder() - .setSuccess(Empty.getDefaultInstance()) - .build(); - - /** - * Sends a "bundle" of messages to an individual destination account, mapping common exceptions to appropriate gRPC - * statuses. - * - * @param messageSender the {@code MessageSender} instance to use to send the messages - * @param destination the destination account for the messages - * @param destinationServiceIdentifier the service identifier for the destination account - * @param messagesByDeviceId a map of device IDs to message payloads - * @param registrationIdsByDeviceId a map of device IDs to device registration IDs - * @param syncMessageSenderDeviceId if the message is a sync message (i.e. a message to other devices linked to the - * caller's own account), contains the ID of the device that sent the message - * - * @return a response object to send to callers - * - * @throws RateLimitExceededException if the message bundle could not be sent due to a violated rated limit - * @throws io.grpc.StatusRuntimeException for invalid arguments if the message is too large to send - */ - public static SendMessageResponse sendMessage(final MessageSender messageSender, - final Account destination, - final ServiceIdentifier destinationServiceIdentifier, - final Map messagesByDeviceId, - final Map registrationIdsByDeviceId, - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional syncMessageSenderDeviceId) - throws RateLimitExceededException { - - try { - messageSender.sendMessages(destination, - destinationServiceIdentifier, - messagesByDeviceId, - registrationIdsByDeviceId, - syncMessageSenderDeviceId, - RequestAttributesUtil.getUserAgent().orElse(null)); - - return SEND_MESSAGE_SUCCESS_RESPONSE; - } catch (final MismatchedDevicesException e) { - return SendMessageResponse.newBuilder() - .setMismatchedDevices(buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices())) - .build(); - } catch (final MessageTooLargeException e) { - throw GrpcExceptions.invalidArguments("message too large"); - } - } - /** * Translates an internal {@link org.whispersystems.textsecuregcm.controllers.MismatchedDevices} entity to a gRPC * {@link MismatchedDevices} entity. diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java index 482d3ab8e..3b6351b4a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcService.java @@ -10,15 +10,17 @@ import java.time.Clock; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import com.google.protobuf.Empty; import org.signal.chat.errors.NotFound; import org.signal.chat.messages.AuthenticatedSenderMessageType; import org.signal.chat.messages.IndividualRecipientMessageBundle; import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest; -import org.signal.chat.messages.SendMessageResponse; +import org.signal.chat.messages.SendMessageAuthenticatedSenderResponse; import org.signal.chat.messages.SendSyncMessageRequest; import org.signal.chat.messages.SimpleMessagesGrpc; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; +import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; @@ -26,13 +28,16 @@ import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.limits.CardinalityEstimator; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.push.MessageSender; -import org.whispersystems.textsecuregcm.spam.GrpcResponse; +import org.whispersystems.textsecuregcm.push.MessageTooLargeException; +import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse; import org.whispersystems.textsecuregcm.spam.MessageType; import org.whispersystems.textsecuregcm.spam.SpamCheckResult; import org.whispersystems.textsecuregcm.spam.SpamChecker; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; +import static org.whispersystems.textsecuregcm.grpc.MessagesGrpcHelper.buildMismatchedDevices; + public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { private final AccountsManager accountsManager; @@ -42,6 +47,9 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { private final SpamChecker spamChecker; private final Clock clock; + private static final SendMessageAuthenticatedSenderResponse SEND_MESSAGE_SUCCESS_RESPONSE = + SendMessageAuthenticatedSenderResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(); + public MessagesGrpcService(final AccountsManager accountsManager, final RateLimiters rateLimiters, final MessageSender messageSender, @@ -58,7 +66,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { } @Override - public SendMessageResponse sendMessage(final SendAuthenticatedSenderMessageRequest request) + public SendMessageAuthenticatedSenderResponse sendMessage(final SendAuthenticatedSenderMessageRequest request) throws RateLimitExceededException { final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); @@ -75,7 +83,9 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { final Optional maybeDestination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier); if (maybeDestination.isEmpty()) { - return SendMessageResponse.newBuilder().setDestinationNotFound(NotFound.getDefaultInstance()).build(); + return SendMessageAuthenticatedSenderResponse.newBuilder() + .setDestinationNotFound(NotFound.getDefaultInstance()) + .build(); } final Account destination = maybeDestination.get(); @@ -92,7 +102,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { } @Override - public SendMessageResponse sendSyncMessage(final SendSyncMessageRequest request) + public SendMessageAuthenticatedSenderResponse sendSyncMessage(final SendSyncMessageRequest request) throws RateLimitExceededException { final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); @@ -110,7 +120,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { request.getUrgent()); } - private SendMessageResponse sendMessage(final Account destination, + private SendMessageAuthenticatedSenderResponse sendMessage(final Account destination, final ServiceIdentifier destinationServiceIdentifier, final AuthenticatedDevice sender, final AuthenticatedSenderMessageType envelopeType, @@ -130,14 +140,16 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { throw e; } - final SpamCheckResult> spamCheckResult = + final SpamCheckResult spamCheckResult = spamChecker.checkForIndividualRecipientSpamGrpc(messageType, Optional.of(sender), Optional.of(destination), destinationServiceIdentifier); if (spamCheckResult.response().isPresent()) { - return spamCheckResult.response().get().getResponseOrThrowStatus(); + return SendMessageAuthenticatedSenderResponse.newBuilder() + .setChallengeRequired(spamCheckResult.response().get().getResponseOrThrowStatus()) + .build(); } final Map messagesByDeviceId = messages.getMessagesMap().entrySet() @@ -168,12 +180,22 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase { entry -> entry.getKey().byteValue(), entry -> entry.getValue().getRegistrationId())); - return MessagesGrpcHelper.sendMessage(messageSender, - destination, - destinationServiceIdentifier, - messagesByDeviceId, - registrationIdsByDeviceId, - messageType == MessageType.SYNC ? Optional.of(sender.deviceId()) : Optional.empty()); + try { + messageSender.sendMessages(destination, + destinationServiceIdentifier, + messagesByDeviceId, + registrationIdsByDeviceId, + messageType == MessageType.SYNC ? Optional.of(sender.deviceId()) : Optional.empty(), + RequestAttributesUtil.getUserAgent().orElse(null)); + + return SEND_MESSAGE_SUCCESS_RESPONSE; + } catch (final MismatchedDevicesException e) { + return SendMessageAuthenticatedSenderResponse.newBuilder() + .setMismatchedDevices(buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices())) + .build(); + } catch (final MessageTooLargeException e) { + throw GrpcExceptions.invalidArguments("message too large"); + } } private static MessageProtos.Envelope.Type getEnvelopeType(final AuthenticatedSenderMessageType type) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcChallengeResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcChallengeResponse.java new file mode 100644 index 000000000..9cf45ac74 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcChallengeResponse.java @@ -0,0 +1,72 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.spam; + +import io.grpc.StatusRuntimeException; +import java.util.function.Function; +import javax.annotation.Nullable; +import org.signal.chat.messages.ChallengeRequired; + +/// A gRPC status or a challenge message to communicate to callers that a message has been flagged as potential spam. +public class GrpcChallengeResponse { + + @Nullable + private final StatusRuntimeException statusException; + + @Nullable + private final ChallengeRequired response; + + private GrpcChallengeResponse(final @Nullable StatusRuntimeException statusException, + @Nullable final ChallengeRequired response) { + this.statusException = statusException; + this.response = response; + if (!((statusException == null) ^ (response == null))) { + throw new IllegalArgumentException("exactly one of statusException and response must be non-null"); + } + } + + /// Constructs a new response object with the given status and no challenge + /// + /// @param status the status to send to callers + /// @return a new response object with the given status and no challenge + public static GrpcChallengeResponse withStatusException(final StatusRuntimeException status) { + return new GrpcChallengeResponse(status, null); + } + + /// Constructs a new response object with the given challenge message. + /// + /// @param response the challenge message to send to the caller + /// @return a new response object with the given challenge message + public static GrpcChallengeResponse withResponse(final ChallengeRequired response) { + return new GrpcChallengeResponse(null, response); + } + + /// Returns the challenge message contained within this response or throws the contained status as a + /// [StatusRuntimeException] if no challenge message is specified. + /// + /// @return the [ChallengeRequired] message + /// @throws StatusRuntimeException if no challenge message is specified + public ChallengeRequired getResponseOrThrowStatus() throws StatusRuntimeException { + if (statusException != null) { + throw statusException; + } + return response; + } + + /// If this response contains a challenge message, throw a status generated using statusMapper. Otherwise, throw the + /// status. + /// + /// @param statusMapper A function that converts a [ChallengeRequired] message into a + /// [StatusRuntimeException] + /// @throws StatusRuntimeException the contained or mapped status exception + public void throwStatusOr(Function statusMapper) + throws StatusRuntimeException { + if (statusException != null) { + throw statusException; + } + throw statusMapper.apply(response); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java deleted file mode 100644 index 90f22f53f..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/spam/GrpcResponse.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.spam; - -import io.grpc.Status; -import io.grpc.StatusException; -import io.grpc.StatusRuntimeException; -import javax.annotation.Nullable; -import java.util.Optional; - -/** - * A combination of a gRPC status and response message to communicate to callers that a message has been flagged as - * potential spam. - * - * @param the type of response object - */ -public class GrpcResponse { - - @Nullable - private final StatusRuntimeException statusException; - - @Nullable - private final R response; - - private GrpcResponse(final @Nullable StatusRuntimeException statusException, @Nullable final R response) { - this.statusException = statusException; - this.response = response; - if (!((statusException == null) ^ (response == null))) { - throw new IllegalArgumentException("exactly one of statusException and response must be non-null"); - } - } - - /** - * Constructs a new response object with the given status and no response message. - * - * @param status the status to send to callers - * - * @return a new response object with the given status and no response message - * - * @param the type of response object - */ - public static GrpcResponse withStatusException(final StatusRuntimeException status) { - return new GrpcResponse<>(status, null); - } - - /** - * Constructs a new response object with a status of {@link Status#OK} and the given response message. - * - * @param response the response to send to the caller - * - * @return a new response object with a status of {@link Status#OK} and the given response message - * - * @param the type of response object - */ - public static GrpcResponse withResponse(final R response) { - return new GrpcResponse<>(null, response); - } - - /** - * Returns the message body contained within this response or throws the contained status as a {@link StatusException} - * if no message body is specified. - * - * @return the message body contained within this response - * - * @throws StatusException if no message body is specified - */ - public R getResponseOrThrowStatus() throws StatusRuntimeException { - if (statusException != null) { - throw statusException; - } - return response; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamChecker.java b/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamChecker.java index 29eec2213..dddb8a530 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamChecker.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/spam/SpamChecker.java @@ -5,10 +5,8 @@ package org.whispersystems.textsecuregcm.spam; import jakarta.ws.rs.container.ContainerRequestContext; -import java.util.Optional; import jakarta.ws.rs.core.Response; -import org.signal.chat.messages.SendMessageResponse; -import org.signal.chat.messages.SendMultiRecipientMessageResponse; +import java.util.Optional; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Account; @@ -55,7 +53,7 @@ public interface SpamChecker { * @return A {@link SpamCheckResult} */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - SpamCheckResult> checkForIndividualRecipientSpamGrpc( + SpamCheckResult checkForIndividualRecipientSpamGrpc( final MessageType messageType, final Optional maybeSource, final Optional maybeDestination, @@ -68,7 +66,7 @@ public interface SpamChecker { * * @return A {@link SpamCheckResult} */ - SpamCheckResult> checkForMultiRecipientSpamGrpc(final MessageType messageType); + SpamCheckResult checkForMultiRecipientSpamGrpc(final MessageType messageType); static SpamChecker noop() { @@ -92,7 +90,7 @@ public interface SpamChecker { } @Override - public SpamCheckResult> checkForIndividualRecipientSpamGrpc(final MessageType messageType, + public SpamCheckResult checkForIndividualRecipientSpamGrpc(final MessageType messageType, final Optional maybeSource, final Optional maybeDestination, final ServiceIdentifier destinationIdentifier) { @@ -101,7 +99,7 @@ public interface SpamChecker { } @Override - public SpamCheckResult> checkForMultiRecipientSpamGrpc( + public SpamCheckResult checkForMultiRecipientSpamGrpc( final MessageType messageType) { return new SpamCheckResult<>(Optional.empty(), Optional.empty()); diff --git a/service/src/main/proto/org/signal/chat/messages.proto b/service/src/main/proto/org/signal/chat/messages.proto index 6df3de2fa..6b6790c44 100644 --- a/service/src/main/proto/org/signal/chat/messages.proto +++ b/service/src/main/proto/org/signal/chat/messages.proto @@ -25,11 +25,11 @@ service Messages { // // The destination account must not be the same as the authenticated caller. // Callers should use `SendSyncMessage` to send messages to themselves. - rpc SendMessage(SendAuthenticatedSenderMessageRequest) returns (SendMessageResponse) {} + rpc SendMessage(SendAuthenticatedSenderMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {} // Sends a "sync" message to all other devices linked to the authenticated // sender's account. - rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageResponse) {} + rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {} } // Provides methods for sending "sealed sender" messages. @@ -133,6 +133,30 @@ message SendAuthenticatedSenderMessageRequest { IndividualRecipientMessageBundle messages = 5; } +message SendMessageAuthenticatedSenderResponse { + + // The outcome of the message delivery + oneof response { + + // The message was successfully delivered to all destination devices + google.protobuf.Empty success = 1; + + // A list of discrepancies between the destination devices identified in a + // request to send a message and the devices that are actually linked to an + // account. + MismatchedDevices mismatched_devices = 2; + + // A description of a challenge callers must complete before sending + // additional messages. + ChallengeRequired challenge_required = 3; + + // The destination account did not exist + errors.NotFound destination_not_found = 4; + + } +} + + message SendSyncMessageRequest { // The type identifier for this message. @@ -202,15 +226,11 @@ message SendMessageResponse { // account. MismatchedDevices mismatched_devices = 2; - // A description of a challenge callers must complete before sending - // additional messages. - ChallengeRequired challenge_required = 3; - // The provided unidentified authorization credential was invalid - errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 4; + errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3; // The destination account did not exist - errors.NotFound destination_not_found = 5; + errors.NotFound destination_not_found = 4; } } @@ -280,12 +300,8 @@ message SendMultiRecipientMessageResponse { // actually linked to a destination account. MultiRecipientMismatchedDevices mismatched_devices = 2; - // A description of a challenge callers must complete before sending - // additional messages. - ChallengeRequired challenge_required = 3; - // The provided unidentified authorization credential was invalid - errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 4; + errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3; } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java index 18839f7aa..3e6210889 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java @@ -65,7 +65,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.push.MessageSender; import org.whispersystems.textsecuregcm.push.MessageTooLargeException; -import org.whispersystems.textsecuregcm.spam.GrpcResponse; +import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse; import org.whispersystems.textsecuregcm.spam.MessageType; import org.whispersystems.textsecuregcm.spam.SpamCheckResult; import org.whispersystems.textsecuregcm.spam.SpamChecker; @@ -427,7 +427,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), + Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -464,16 +464,16 @@ class MessagesAnonymousGrpcServiceTest extends .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) .build()); - final SendMessageResponse response = SendMessageResponse.newBuilder() - .setChallengeRequired(ChallengeRequired.newBuilder() - .addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA)) - .build(); + final ChallengeRequired challengeResponse = + ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build(); when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty())); + .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); - assertEquals(response, unauthenticatedServiceStub().sendSingleRecipientMessage( - generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null))); + final SendSealedSenderMessageRequest request = + generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null); + GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> + unauthenticatedServiceStub().sendSingleRecipientMessage(request)); verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_SEALED_SENDER, Optional.empty(), @@ -793,7 +793,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForMultiRecipientSpamGrpc(any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), + Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -837,15 +837,14 @@ class MessagesAnonymousGrpcServiceTest extends .setUrgent(true) .build(); - final SendMultiRecipientMessageResponse response = SendMultiRecipientMessageResponse.newBuilder() - .setChallengeRequired(ChallengeRequired.newBuilder() - .addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA)) - .build(); - + final ChallengeRequired challengeResponse = + ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build(); when(spamChecker.checkForMultiRecipientSpamGrpc(any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty())); + .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); - assertEquals(response, unauthenticatedServiceStub().sendMultiRecipientMessage(request)); + //noinspection ResultOfMethodCallIgnored + GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, + () -> unauthenticatedServiceStub().sendMultiRecipientMessage(request)); verify(spamChecker).checkForMultiRecipientSpamGrpc(MessageType.MULTI_RECIPIENT_SEALED_SENDER); @@ -1052,7 +1051,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), + Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -1087,16 +1086,13 @@ class MessagesAnonymousGrpcServiceTest extends .setPayload(ByteString.copyFrom(TestRandomUtil.nextBytes(128))) .build()); - final SendMessageResponse response = SendMessageResponse.newBuilder() - .setChallengeRequired(ChallengeRequired.newBuilder() - .addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA)) - .build(); - + final ChallengeRequired challengeResponse = + ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build(); when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty())); + .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); - assertEquals(response, unauthenticatedServiceStub().sendStory( - generateRequest(serviceIdentifier, true, messages))); + GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> + unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, true, messages))); verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_STORY, Optional.empty(), @@ -1335,7 +1331,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForMultiRecipientSpamGrpc(any())) .thenReturn(new SpamCheckResult<>( - Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), + Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -1377,15 +1373,13 @@ class MessagesAnonymousGrpcServiceTest extends .setUrgent(true) .build(); - final SendMultiRecipientMessageResponse response = SendMultiRecipientMessageResponse.newBuilder() - .setChallengeRequired(ChallengeRequired.newBuilder() - .addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA)) - .build(); - + final ChallengeRequired challengeResponse = + ChallengeRequired.newBuilder().addChallengeOptions(ChallengeRequired.ChallengeType.CAPTCHA).build(); when(spamChecker.checkForMultiRecipientSpamGrpc(any())) - .thenReturn(new SpamCheckResult<>(Optional.of(GrpcResponse.withResponse(response)), Optional.empty())); + .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); - assertEquals(response, unauthenticatedServiceStub().sendMultiRecipientStory(request)); + GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> + unauthenticatedServiceStub().sendMultiRecipientStory(request)); verify(spamChecker).checkForMultiRecipientSpamGrpc(MessageType.MULTI_RECIPIENT_STORY); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java index 03439fa5f..54d701c6e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java @@ -39,7 +39,7 @@ import org.signal.chat.messages.IndividualRecipientMessageBundle; import org.signal.chat.messages.MessagesGrpc; import org.signal.chat.messages.MismatchedDevices; import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest; -import org.signal.chat.messages.SendMessageResponse; +import org.signal.chat.messages.SendMessageAuthenticatedSenderResponse; import org.signal.chat.messages.SendSyncMessageRequest; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; @@ -53,7 +53,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.push.MessageSender; import org.whispersystems.textsecuregcm.push.MessageTooLargeException; -import org.whispersystems.textsecuregcm.spam.GrpcResponse; +import org.whispersystems.textsecuregcm.spam.GrpcChallengeResponse; import org.whispersystems.textsecuregcm.spam.MessageType; import org.whispersystems.textsecuregcm.spam.SpamCheckResult; import org.whispersystems.textsecuregcm.spam.SpamChecker; @@ -184,10 +184,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest MessageProtos.Envelope.Type.CIPHERTEXT; @@ -245,10 +245,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest( - Optional.of(GrpcResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), + Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); //noinspection ResultOfMethodCallIgnored @@ -395,15 +395,17 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest(Optional.of(GrpcResponse.withResponse(response)), Optional.empty())); + .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeRequired)), Optional.empty())); - assertEquals(response, authenticatedServiceStub().sendMessage( + assertEquals(expectedResponse, authenticatedServiceStub().sendMessage( generateRequest(serviceIdentifier, AuthenticatedSenderMessageType.DOUBLE_RATCHET, false, true, messages))); verify(spamChecker).checkForIndividualRecipientSpamGrpc(MessageType.INDIVIDUAL_IDENTIFIED_SENDER, @@ -466,10 +468,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest(Optional.empty(), Optional.of(reportSpamToken))); } - final SendMessageResponse response = + final SendMessageAuthenticatedSenderResponse response = authenticatedServiceStub().sendSyncMessage(generateRequest(messageType, urgent, messages)); - assertEquals(SendMessageResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response); + assertEquals(SendMessageAuthenticatedSenderResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build(), response); final MessageProtos.Envelope.Type expectedEnvelopeType = switch (messageType) { case DOUBLE_RATCHET -> MessageProtos.Envelope.Type.CIPHERTEXT; @@ -539,10 +541,10 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest