diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 536e8271a..8db4c8d03 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -743,7 +743,7 @@ public class WhisperServerService extends Application dynamicConfigurationManager; private final List messageDeliveryListeners = new ArrayList<>(); @@ -80,9 +84,12 @@ public class MessageSender { @VisibleForTesting static final byte NO_EXCLUDED_DEVICE_ID = -1; - public MessageSender(final MessagesManager messagesManager, final PushNotificationManager pushNotificationManager) { + public MessageSender(final MessagesManager messagesManager, + final PushNotificationManager pushNotificationManager, + final DynamicConfigurationManager dynamicConfigurationManager) { this.messagesManager = messagesManager; this.pushNotificationManager = pushNotificationManager; + this.dynamicConfigurationManager = dynamicConfigurationManager; } public void addMessageDeliveryListener(final MessageDeliveryListener messageDeliveryListener) { @@ -112,7 +119,12 @@ public class MessageSender { final Map messagesByDeviceId, final Map registrationIdsByDeviceId, @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional syncMessageSenderDeviceId, - @Nullable final String userAgent) throws MismatchedDevicesException, MessageTooLargeException { + @Nullable final String userAgent) + throws MismatchedDevicesException, MessageTooLargeException, MessageDeliveryNotAllowedException { + + if (dynamicConfigurationManager.getConfiguration().getMessageDeliveryConfiguration().isReadOnly()) { + throw new MessageDeliveryNotAllowedException(); + } final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent); @@ -189,7 +201,12 @@ public class MessageSender { final boolean isStory, final boolean isEphemeral, final boolean isUrgent, - @Nullable final String userAgent) throws MultiRecipientMismatchedDevicesException, MessageTooLargeException { + @Nullable final String userAgent) + throws MultiRecipientMismatchedDevicesException, MessageTooLargeException, MessageDeliveryNotAllowedException { + + if (dynamicConfigurationManager.getConfiguration().getMessageDeliveryConfiguration().isReadOnly()) { + throw new MessageDeliveryNotAllowedException(); + } final Tag platformTag = UserAgentTagUtil.getPlatformTag(userAgent); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java index 0756d8013..9f9e28b1b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; import org.whispersystems.textsecuregcm.controllers.AccountControllerV2; +import org.whispersystems.textsecuregcm.controllers.MessageDeliveryNotAllowedException; import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; @@ -81,7 +82,7 @@ public class ChangeNumberManager { final List deviceMessages, final Map pniRegistrationIds, final ContainerRequestContext containerRequestContext) - throws InterruptedException, MismatchedDevicesException, MessageTooLargeException, RateLimitExceededException { + throws InterruptedException, MismatchedDevicesException, MessageTooLargeException, RateLimitExceededException, MessageDeliveryNotAllowedException { final String senderUserAgent = containerRequestContext.getHeaderString(HttpHeaders.USER_AGENT); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java index b72c62f7b..0ca886d1f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerV2Test.java @@ -292,7 +292,7 @@ class AccountControllerV2Test { @ParameterizedTest @MethodSource void phoneVerificationException(final Exception exception, final int expectedStatus) - throws InterruptedException, MessageTooLargeException, MismatchedDevicesException, RateLimitExceededException { + throws InterruptedException, MessageTooLargeException, MismatchedDevicesException, RateLimitExceededException, MessageDeliveryNotAllowedException { doThrow(exception) .when(changeNumberManager).changeNumber(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); @@ -338,6 +338,28 @@ class AccountControllerV2Test { } } + @Test + void messageDeliveryNotAllowed() throws Exception { + doThrow(MessageDeliveryNotAllowedException.class) + .when(changeNumberManager).changeNumber(any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any()); + + try (final Response response = resources.getJerseyTest() + .target("/v2/accounts/number") + .request() + .header(HttpHeaders.AUTHORIZATION, + AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity( + new ChangeNumberRequest(encodeSessionId("session"), null, NEW_NUMBER, "123", IDENTITY_KEY, + Collections.emptyList(), + Map.of(Device.PRIMARY_ID, KeysHelper.signedECPreKey(1, IDENTITY_KEY_PAIR)), + Map.of(Device.PRIMARY_ID, KeysHelper.signedKEMPreKey(2, IDENTITY_KEY_PAIR)), + Map.of(Device.PRIMARY_ID, 17)), + MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(503, response.getStatus()); + } + } + /** * Valid request JSON with the given Recovery Password */ diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java index bb17988a6..a4d4308cc 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java @@ -196,7 +196,8 @@ class MessageControllerTest { .build(); @BeforeEach - void setup() throws MultiRecipientMismatchedDevicesException, MessageTooLargeException { + void setup() + throws MultiRecipientMismatchedDevicesException, MessageTooLargeException, MessageDeliveryNotAllowedException { reset(pushNotificationScheduler); when(messageSender.sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any())) @@ -545,6 +546,24 @@ class MessageControllerTest { } } + @Test + void testSingleDeviceMessageDeliveryNotAllowed() throws Exception { + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); + + try (final Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .put(Entity.entity(SystemMapper.jsonMapper().readValue(jsonFixture("fixtures/current_message_single_device.json"), + IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE))) { + + assertThat(response.getStatus(), is(equalTo(503))); + } + } + @Test void testSendBadAuth() throws Exception { try (final Response response = @@ -1042,7 +1061,8 @@ class MessageControllerTest { } @Test - void testValidateContentLength() throws MismatchedDevicesException, MessageTooLargeException, IOException { + void testValidateContentLength() + throws MismatchedDevicesException, MessageTooLargeException, IOException, MessageDeliveryNotAllowedException { doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); try (final Response response = @@ -1101,7 +1121,7 @@ class MessageControllerTest { final Set expectedResolvedAccounts, final Set expectedUuids404, @Nullable final MultiRecipientMismatchedDevicesException mismatchedDevicesException) - throws MultiRecipientMismatchedDevicesException, MessageTooLargeException { + throws MultiRecipientMismatchedDevicesException, MessageTooLargeException, MessageDeliveryNotAllowedException { clock.pin(START_OF_DAY); @@ -1674,6 +1694,72 @@ class MessageControllerTest { } } + @Test + void sendMultiRecipientMessageMessageDeliveryNotAllowed() throws Exception { + + clock.pin(START_OF_DAY); + + final UUID singleDeviceAccountAci = UUID.randomUUID(); + final UUID singleDeviceAccountPni = UUID.randomUUID(); + + final byte[] singleDeviceAccountUak = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH); + + final int singleDevicePrimaryRegistrationId = 1; + + final Device singleDeviceAccountPrimary = mock(Device.class); + when(singleDeviceAccountPrimary.getId()).thenReturn(Device.PRIMARY_ID); + when(singleDeviceAccountPrimary.getRegistrationId(IdentityType.ACI)).thenReturn(singleDevicePrimaryRegistrationId); + + final Account singleDeviceAccount = mock(Account.class); + when(singleDeviceAccount.getIdentifier(IdentityType.ACI)).thenReturn(singleDeviceAccountAci); + when(singleDeviceAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(singleDeviceAccountUak)); + when(singleDeviceAccount.getDevices()).thenReturn(List.of(singleDeviceAccountPrimary)); + when(singleDeviceAccount.getDevice(anyByte())).thenReturn(Optional.empty()); + when(singleDeviceAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(singleDeviceAccountPrimary)); + + final Map accountsByServiceIdentifier = Map.of( + new AciServiceIdentifier(singleDeviceAccountAci), singleDeviceAccount, + new PniServiceIdentifier(singleDeviceAccountPni), singleDeviceAccount); + + final byte[] aciMessage = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of( + new TestRecipient(new AciServiceIdentifier(singleDeviceAccountAci), Device.PRIMARY_ID, singleDevicePrimaryRegistrationId, new byte[48]))); + + when(accountsManager.getByServiceIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + accountsByServiceIdentifier.forEach(((serviceIdentifier, account) -> + when(accountsManager.getByServiceIdentifierAsync(serviceIdentifier)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(account))))); + + final boolean ephemeral = true; + final boolean urgent = false; + final boolean story = false; + + final Invocation.Builder invocationBuilder = resources + .getJerseyTest() + .target("/v1/messages/multi_recipient") + .queryParam("ts", clock.millis()) + .queryParam("online", ephemeral) + .queryParam("story", story) + .queryParam("urgent", urgent) + .request() + .header(HeaderUtils.GROUP_SEND_TOKEN, AuthHelper.validGroupSendTokenHeader(serverSecretParams, + List.of(new AciServiceIdentifier(singleDeviceAccountAci)), + START_OF_DAY.plus(Duration.ofDays(1)))); + + when(rateLimiter.validateAsync(any(UUID.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + + try (final Response response = invocationBuilder + .put(Entity.entity(aciMessage, MultiRecipientMessageProvider.MEDIA_TYPE))) { + + assertThat(response.getStatus(), is(equalTo(503))); + } + } + @SuppressWarnings("SameParameterValue") private static Envelope generateEnvelope(UUID guid, int type, long timestamp, UUID sourceUuid, byte sourceDevice, UUID destinationUuid, UUID updatedPni, byte[] content, long serverTimestamp) { 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 b91bbb877..6e860b12a 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesAnonymousGrpcServiceTest.java @@ -54,6 +54,7 @@ import org.signal.chat.messages.SendMultiRecipientStoryRequest; import org.signal.chat.messages.SendSealedSenderMessageRequest; import org.signal.chat.messages.SendStoryMessageRequest; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; +import org.whispersystems.textsecuregcm.controllers.MessageDeliveryNotAllowedException; import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.MultiRecipientMismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; @@ -156,7 +157,7 @@ class MessagesAnonymousGrpcServiceTest extends @CartesianTest.Values(booleans = {true, false}) final boolean ephemeral, @CartesianTest.Values(booleans = {true, false}) final boolean urgent, @CartesianTest.Values(booleans = {true, false}) final boolean includeReportSpamToken) - throws MessageTooLargeException, MismatchedDevicesException { + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -244,7 +245,8 @@ class MessagesAnonymousGrpcServiceTest extends .setType(SendMessageType.DOUBLE_RATCHET) .setPayload(ByteString.copyFrom(payload)) .build()); - final byte[] reportSpamToken = TestRandomUtil.nextBytes(64); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendSingleRecipientMessage( generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null))); @@ -254,8 +256,7 @@ class MessagesAnonymousGrpcServiceTest extends @CartesianTest void sendUnrestrictedAccessMessage( @CartesianTest.Values(booleans = {true, false}) final boolean useUak, - @CartesianTest.Values(booleans = {true, false}) final boolean isUua) - throws MessageTooLargeException, MismatchedDevicesException { + @CartesianTest.Values(booleans = {true, false}) final boolean isUua) { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -289,7 +290,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void mismatchedDevices() throws MessageTooLargeException, MismatchedDevicesException { + void mismatchedDevices() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte missingDeviceId = Device.PRIMARY_ID; final byte extraDeviceId = missingDeviceId + 1; final byte staleDeviceId = extraDeviceId + 1; @@ -328,7 +330,8 @@ class MessagesAnonymousGrpcServiceTest extends @ParameterizedTest @ValueSource(booleans = {true, false}) - void badCredentials(final boolean useUak) throws MessageTooLargeException, MismatchedDevicesException { + void badCredentials(final boolean useUak) + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -367,7 +370,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void destinationNotFound() throws MessageTooLargeException, MismatchedDevicesException { + void destinationNotFound() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); final Map messages = @@ -388,7 +392,7 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void pniIdentifierWithUak() throws MessageTooLargeException, MismatchedDevicesException { + void pniIdentifierWithUak() { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; final Device destinationDevice = DevicesHelper.createDevice(deviceId, CLOCK.millis(), registrationId); @@ -410,13 +414,15 @@ class MessagesAnonymousGrpcServiceTest extends final SendSealedSenderMessageRequest request = generateRequest(pniIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null); + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException( Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendSingleRecipientMessage(request)); } @Test - void rateLimited() throws RateLimitExceededException, MessageTooLargeException, MismatchedDevicesException { + void rateLimited() + throws RateLimitExceededException, MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -450,7 +456,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void oversizedMessage() throws MessageTooLargeException, MismatchedDevicesException { + void oversizedMessage() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte missingDeviceId = Device.PRIMARY_ID; final byte extraDeviceId = missingDeviceId + 1; final byte staleDeviceId = extraDeviceId + 1; @@ -470,14 +477,15 @@ class MessagesAnonymousGrpcServiceTest extends doThrow(new MessageTooLargeException()) .when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendSingleRecipientMessage( generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null))); } @Test - void spamWithStatus() throws MessageTooLargeException, MismatchedDevicesException { + void spamWithStatus() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -502,7 +510,7 @@ class MessagesAnonymousGrpcServiceTest extends Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendSingleRecipientMessage( generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null))); @@ -516,7 +524,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void spamWithResponse() throws MessageTooLargeException, MismatchedDevicesException { + void spamWithResponse() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -544,6 +553,8 @@ class MessagesAnonymousGrpcServiceTest extends final SendSealedSenderMessageRequest request = generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendSingleRecipientMessage(request)); @@ -555,6 +566,42 @@ class MessagesAnonymousGrpcServiceTest extends verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); } + @Test + void messageDeliveryNotAllowed() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { + final byte deviceId = Device.PRIMARY_ID; + final int registrationId = 7; + + final Device destinationDevice = DevicesHelper.createDevice(deviceId, CLOCK.millis(), registrationId); + + final Account destinationAccount = mock(Account.class); + when(destinationAccount.getDevices()).thenReturn(List.of(destinationDevice)); + when(destinationAccount.getDevice(deviceId)).thenReturn(Optional.of(destinationDevice)); + when(destinationAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); + + final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); + when(accountsManager.getByServiceIdentifier(serviceIdentifier)).thenReturn(Optional.of(destinationAccount)); + + final byte[] payload = TestRandomUtil.nextBytes(128); + + final Map messages = + Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(registrationId) + .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.UNIDENTIFIED_SENDER) + .build()); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown + GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, + () -> unauthenticatedServiceStub().sendSingleRecipientMessage( + generateRequest(serviceIdentifier, false, false, messages, + null, + GROUP_SEND_TOKEN))); + } + private static SendSealedSenderMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier, final boolean ephemeral, final boolean urgent, @@ -595,7 +642,7 @@ class MessagesAnonymousGrpcServiceTest extends @CartesianTest void sendMessage(@CartesianTest.Values(booleans = {true, false}) final boolean ephemeral, @CartesianTest.Values(booleans = {true, false}) final boolean urgent) - throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -656,7 +703,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void mismatchedDevices() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void mismatchedDevices() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte missingDeviceId = Device.PRIMARY_ID; final byte extraDeviceId = missingDeviceId + 1; final byte staleDeviceId = extraDeviceId + 1; @@ -704,7 +752,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void badCredentials() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void badCredentials() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -739,7 +788,7 @@ class MessagesAnonymousGrpcServiceTest extends .setUrgent(true) .build())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(SendMultiRecipientMessageRequest.newBuilder() .setMessage(MultiRecipientMessage.newBuilder() @@ -755,8 +804,9 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void badPayload() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { - //noinspection ResultOfMethodCallIgnored + void badPayload() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(SendMultiRecipientMessageRequest.newBuilder() .setMessage(MultiRecipientMessage.newBuilder() @@ -765,7 +815,7 @@ class MessagesAnonymousGrpcServiceTest extends .build()) .build())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(SendMultiRecipientMessageRequest.newBuilder().build())); @@ -774,7 +824,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void repeatedRecipient() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void repeatedRecipient() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final Device destinationDevice = DevicesHelper.createDevice(Device.PRIMARY_ID, CLOCK.millis(), 1); final Account destinationAccount = mock(Account.class); @@ -790,7 +841,7 @@ class MessagesAnonymousGrpcServiceTest extends final byte[] payload = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of(recipient, recipient)); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(SendMultiRecipientMessageRequest.newBuilder() .setGroupSendToken(ByteString.copyFrom(GROUP_SEND_TOKEN)) @@ -807,7 +858,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void oversizedMessage() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void oversizedMessage() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final Account destinationAccount = mock(Account.class); final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); @@ -831,13 +883,14 @@ class MessagesAnonymousGrpcServiceTest extends doThrow(new MessageTooLargeException()) .when(messageSender).sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(request)); } @Test - void spamWithStatus() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void spamWithStatus() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -872,7 +925,7 @@ class MessagesAnonymousGrpcServiceTest extends Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(request)); @@ -883,7 +936,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void spamWithResponse() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void spamWithResponse() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -918,7 +972,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForMultiRecipientSpamGrpc(any())) .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendMultiRecipientMessage(request)); @@ -927,6 +981,41 @@ class MessagesAnonymousGrpcServiceTest extends verify(messageSender, never()) .sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); } + + @Test + void messageDeliveryNotAllowed() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { + final byte missingDeviceId = Device.PRIMARY_ID; + final byte extraDeviceId = missingDeviceId + 1; + final byte staleDeviceId = extraDeviceId + 1; + + final Account destinationAccount = mock(Account.class); + + final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); + + when(accountsManager.getByServiceIdentifierAsync(serviceIdentifier)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(destinationAccount))); + + final byte[] payload = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of( + new TestRecipient(serviceIdentifier, staleDeviceId, 17, new byte[48]))); + + final SendMultiRecipientMessageRequest request = SendMultiRecipientMessageRequest.newBuilder() + .setGroupSendToken(ByteString.copyFrom(GROUP_SEND_TOKEN)) + .setMessage(MultiRecipientMessage.newBuilder() + .setTimestamp(CLOCK.millis()) + .setPayload(ByteString.copyFrom(payload)) + .build()) + .setEphemeral(false) + .setUrgent(true) + .build(); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown + GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, + () -> unauthenticatedServiceStub().sendMultiRecipientMessage(request)); + } } @Nested @@ -935,7 +1024,7 @@ class MessagesAnonymousGrpcServiceTest extends @CartesianTest void sendStory(@CartesianTest.Values(booleans = {true, false}) final boolean urgent, @CartesianTest.Values(booleans = {true, false}) final boolean includeReportSpamToken) - throws MessageTooLargeException, MismatchedDevicesException { + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -998,7 +1087,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void mismatchedDevices() throws MessageTooLargeException, MismatchedDevicesException { + void mismatchedDevices() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte missingDeviceId = Device.PRIMARY_ID; final byte extraDeviceId = missingDeviceId + 1; final byte staleDeviceId = extraDeviceId + 1; @@ -1036,7 +1126,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void destinationNotFound() throws MessageTooLargeException, MismatchedDevicesException { + void destinationNotFound() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final Map messages = Map.of(Device.PRIMARY_ID, IndividualRecipientMessageBundle.Message.newBuilder() .setRegistrationId(7) @@ -1053,7 +1144,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void rateLimited() throws RateLimitExceededException, MessageTooLargeException, MismatchedDevicesException { + void rateLimited() + throws RateLimitExceededException, MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -1085,7 +1177,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void oversizedMessage() throws MessageTooLargeException, MismatchedDevicesException { + void oversizedMessage() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte missingDeviceId = Device.PRIMARY_ID; final byte extraDeviceId = missingDeviceId + 1; final byte staleDeviceId = extraDeviceId + 1; @@ -1105,13 +1198,14 @@ class MessagesAnonymousGrpcServiceTest extends doThrow(new MessageTooLargeException()).when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusInvalidArgument( () -> unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, false, messages))); } @Test - void spamWithStatus() throws MessageTooLargeException, MismatchedDevicesException { + void spamWithStatus() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -1136,7 +1230,7 @@ class MessagesAnonymousGrpcServiceTest extends Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, true, messages))); @@ -1149,7 +1243,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void spamWithResponse() throws MessageTooLargeException, MismatchedDevicesException { + void spamWithResponse() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -1174,6 +1269,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForIndividualRecipientSpamGrpc(any(), any(), any(), any())) .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, true, messages))); @@ -1185,6 +1281,38 @@ class MessagesAnonymousGrpcServiceTest extends verify(messageSender, never()).sendMessages(any(), any(), any(), any(), any(), any()); } + @Test + void messageDeliveryNotAllowed() + throws MessageTooLargeException, MessageDeliveryNotAllowedException, MismatchedDevicesException { + final byte deviceId = Device.PRIMARY_ID; + final int registrationId = 7; + + final Device destinationDevice = DevicesHelper.createDevice(deviceId, CLOCK.millis(), registrationId); + + final Account destinationAccount = mock(Account.class); + when(destinationAccount.getDevices()).thenReturn(List.of(destinationDevice)); + when(destinationAccount.getDevice(deviceId)).thenReturn(Optional.of(destinationDevice)); + + final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); + when(accountsManager.getByServiceIdentifier(serviceIdentifier)).thenReturn(Optional.of(destinationAccount)); + + final byte[] payload = TestRandomUtil.nextBytes(128); + + final Map messages = + Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(registrationId) + .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.UNIDENTIFIED_SENDER) + .build()); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown + GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, + () -> unauthenticatedServiceStub().sendStory(generateRequest(serviceIdentifier, false, messages))); + } + private static SendStoryMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier, final boolean urgent, final Map messages) { @@ -1207,7 +1335,8 @@ class MessagesAnonymousGrpcServiceTest extends @ParameterizedTest @ValueSource(booleans = {true, false}) - void sendStory(final boolean urgent) throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void sendStory(final boolean urgent) + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -1260,7 +1389,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void mismatchedDevices() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void mismatchedDevices() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte missingDeviceId = Device.PRIMARY_ID; final byte extraDeviceId = missingDeviceId + 1; final byte staleDeviceId = extraDeviceId + 1; @@ -1306,8 +1436,9 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void badPayload() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { - //noinspection ResultOfMethodCallIgnored + void badPayload() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientStory(SendMultiRecipientStoryRequest.newBuilder() .setMessage(MultiRecipientMessage.newBuilder() @@ -1316,7 +1447,7 @@ class MessagesAnonymousGrpcServiceTest extends .build()) .build())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientMessage( SendMultiRecipientMessageRequest.newBuilder().build())); @@ -1326,7 +1457,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void repeatedRecipient() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void repeatedRecipient() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final Device destinationDevice = DevicesHelper.createDevice(Device.PRIMARY_ID, CLOCK.millis(), 1); final Account destinationAccount = mock(Account.class); @@ -1342,7 +1474,7 @@ class MessagesAnonymousGrpcServiceTest extends final byte[] payload = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of(recipient, recipient)); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT, () -> unauthenticatedServiceStub().sendMultiRecipientStory(SendMultiRecipientStoryRequest.newBuilder() .setMessage(MultiRecipientMessage.newBuilder() @@ -1357,7 +1489,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void oversizedMessage() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void oversizedMessage() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final Account destinationAccount = mock(Account.class); final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); @@ -1379,12 +1512,13 @@ class MessagesAnonymousGrpcServiceTest extends doThrow(new MessageTooLargeException()) .when(messageSender).sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusInvalidArgument(() -> unauthenticatedServiceStub().sendMultiRecipientStory(request)); } @Test - void spamWithStatus() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void spamWithStatus() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -1417,7 +1551,7 @@ class MessagesAnonymousGrpcServiceTest extends Optional.of(GrpcChallengeResponse.withStatusException(GrpcExceptions.rateLimitExceeded(null))), Optional.empty())); - //noinspection ResultOfMethodCallIgnored + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendMultiRecipientStory(request)); @@ -1428,7 +1562,8 @@ class MessagesAnonymousGrpcServiceTest extends } @Test - void spamWithResponse() throws MessageTooLargeException, MultiRecipientMismatchedDevicesException { + void spamWithResponse() + throws MessageTooLargeException, MultiRecipientMismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -1461,6 +1596,7 @@ class MessagesAnonymousGrpcServiceTest extends when(spamChecker.checkForMultiRecipientSpamGrpc(any())) .thenReturn(new SpamCheckResult<>(Optional.of(GrpcChallengeResponse.withResponse(challengeResponse)), Optional.empty())); + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown GrpcTestUtils.assertStatusException(Status.RESOURCE_EXHAUSTED, () -> unauthenticatedServiceStub().sendMultiRecipientStory(request)); @@ -1469,5 +1605,47 @@ class MessagesAnonymousGrpcServiceTest extends verify(messageSender, never()) .sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); } + + @Test + void messageDeliveryNotAllowed() + throws MessageTooLargeException, MessageDeliveryNotAllowedException, MultiRecipientMismatchedDevicesException { + final byte deviceId = Device.PRIMARY_ID; + final int registrationId = 7; + + final Device destinationDevice = DevicesHelper.createDevice(deviceId, CLOCK.millis(), registrationId); + + final Account resolvedAccount = mock(Account.class); + when(resolvedAccount.getDevices()).thenReturn(List.of(destinationDevice)); + when(resolvedAccount.getDevice(deviceId)).thenReturn(Optional.of(destinationDevice)); + + final AciServiceIdentifier resolvedServiceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); + final AciServiceIdentifier unresolvedServiceIdentifier = new AciServiceIdentifier(UUID.randomUUID()); + + when(accountsManager.getByServiceIdentifierAsync(resolvedServiceIdentifier)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(resolvedAccount))); + + final TestRecipient resolvedRecipient = + new TestRecipient(resolvedServiceIdentifier, deviceId, registrationId, new byte[48]); + + final TestRecipient unresolvedRecipient = + new TestRecipient(unresolvedServiceIdentifier, Device.PRIMARY_ID, 1, new byte[48]); + + final byte[] payload = MultiRecipientMessageHelper.generateMultiRecipientMessage(List.of( + resolvedRecipient, unresolvedRecipient)); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMultiRecipientMessage(any(), any(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + + final SendMultiRecipientStoryRequest request = SendMultiRecipientStoryRequest.newBuilder() + .setMessage(MultiRecipientMessage.newBuilder() + .setTimestamp(CLOCK.millis()) + .setPayload(ByteString.copyFrom(payload)) + .build()) + .build(); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown + GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, + () -> unauthenticatedServiceStub().sendMultiRecipientStory(request)); + } } } 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 59857a06f..423186542 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/MessagesGrpcServiceTest.java @@ -43,6 +43,7 @@ import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest; import org.signal.chat.messages.SendMessageAuthenticatedSenderResponse; import org.signal.chat.messages.SendSyncMessageRequest; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.controllers.MessageDeliveryNotAllowedException; import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.MessageProtos; @@ -156,7 +157,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub() .sendMessage(generateRequest(serviceIdentifier, false, true, messages))); @@ -257,7 +259,8 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest messages = @@ -312,7 +316,8 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().sendMessage( generateRequest(serviceIdentifier, false, true, messages))); } @Test - void spamWithStatus() throws MessageTooLargeException, MismatchedDevicesException { + void spamWithStatus() + throws MessageTooLargeException, MismatchedDevicesException, MessageDeliveryNotAllowedException { final byte deviceId = Device.PRIMARY_ID; final int registrationId = 7; @@ -399,7 +406,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub() .sendMessage(generateRequest(serviceIdentifier, false, true, messages))); @@ -412,7 +419,8 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest messages = + Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(registrationId) + .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.DOUBLE_RATCHET) + .build()); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown + GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, + () -> authenticatedServiceStub().sendMessage(generateRequest(serviceIdentifier, false, false, messages))); + } + private static SendAuthenticatedSenderMessageRequest generateRequest(final ServiceIdentifier serviceIdentifier, final boolean ephemeral, final boolean urgent, @@ -480,7 +520,7 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest + expectedEnvelopes.replaceAll((_, envelope) -> envelope.toBuilder().setReportSpamToken(ByteString.copyFrom(reportSpamToken)).build()); } @@ -563,7 +603,8 @@ class MessagesGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub() .sendSyncMessage( generateRequest( true, messages))); } + @Test + void messageDeliveryNotAllowed() + throws MessageTooLargeException, MessageDeliveryNotAllowedException, MismatchedDevicesException { + final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(AUTHENTICATED_ACI); + final byte[] payload = TestRandomUtil.nextBytes(128); + + final Map messages = + Map.of(LINKED_DEVICE_ID, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(LINKED_DEVICE_REGISTRATION_ID) + .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.DOUBLE_RATCHET) + .build(), + + SECOND_LINKED_DEVICE_ID, IndividualRecipientMessageBundle.Message.newBuilder() + .setRegistrationId(SECOND_LINKED_DEVICE_REGISTRATION_ID) + .setPayload(ByteString.copyFrom(payload)) + .setType(SendMessageType.DOUBLE_RATCHET) + .build()); + + doThrow(MessageDeliveryNotAllowedException.class) + .when(messageSender).sendMessages(any(), any(), any(), any(), any(), any()); + + //noinspection ResultOfMethodCallIgnored,ThrowableNotThrown + GrpcTestUtils.assertStatusException(Status.UNAVAILABLE, + () -> authenticatedServiceStub().sendSyncMessage(generateRequest(false, messages))); + } + private static SendSyncMessageRequest generateRequest( final boolean urgent, final Map messages) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java index 72d09e052..1ced7f97c 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java @@ -39,6 +39,9 @@ import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidVersionException; import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicMessageDeliveryConfiguration; +import org.whispersystems.textsecuregcm.controllers.MessageDeliveryNotAllowedException; import org.whispersystems.textsecuregcm.controllers.MismatchedDevices; import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.MultiRecipientMismatchedDevicesException; @@ -51,6 +54,7 @@ import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.spam.MessageDeliveryListener; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.tests.util.MultiRecipientMessageHelper; import org.whispersystems.textsecuregcm.tests.util.TestRecipient; @@ -62,6 +66,8 @@ class MessageSenderTest { private PushNotificationManager pushNotificationManager; private MessageDeliveryListener messageDeliveryListener; + private DynamicMessageDeliveryConfiguration dynamicMessageDeliveryConfiguration; + private MessageSender messageSender; @BeforeEach @@ -70,7 +76,17 @@ class MessageSenderTest { pushNotificationManager = mock(PushNotificationManager.class); messageDeliveryListener = mock(MessageDeliveryListener.class); - messageSender = new MessageSender(messagesManager, pushNotificationManager); + dynamicMessageDeliveryConfiguration = mock(DynamicMessageDeliveryConfiguration.class); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + when(dynamicConfiguration.getMessageDeliveryConfiguration()).thenReturn(dynamicMessageDeliveryConfiguration); + + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + + messageSender = new MessageSender(messagesManager, pushNotificationManager, dynamicConfigurationManager); messageSender.addMessageDeliveryListener(messageDeliveryListener); } @@ -182,6 +198,36 @@ class MessageSenderTest { anyBoolean()); } + @Test + void sendMessageReadOnlyMode() { + final UUID accountIdentifier = UUID.randomUUID(); + final ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(accountIdentifier); + final byte deviceId = Device.PRIMARY_ID; + final int registrationId = 17; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + final MessageProtos.Envelope message = MessageProtos.Envelope.newBuilder().build(); + + when(account.getUuid()).thenReturn(accountIdentifier); + when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier); + when(account.isIdentifiedBy(serviceIdentifier)).thenReturn(true); + when(account.getDevices()).thenReturn(List.of(device)); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(device.getId()).thenReturn(deviceId); + when(device.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId); + when(device.getApnId()).thenReturn("apns-token"); + + when(dynamicMessageDeliveryConfiguration.isReadOnly()).thenReturn(true); + + assertThrows(MessageDeliveryNotAllowedException.class, () -> messageSender.sendMessages(account, + serviceIdentifier, + Map.of(device.getId(), message), + Map.of(device.getId(), registrationId), + Optional.empty(), + null)); + } + @CartesianTest void sendMultiRecipientMessage(@CartesianTest.Values(booleans = {true, false}) final boolean clientPresent, @CartesianTest.Values(booleans = {true, false}) final boolean ephemeral, @@ -303,6 +349,48 @@ class MessageSenderTest { anyBoolean()); } + @Test + void sendMultiRecipientMessageReadOnlyMode() + throws NotPushRegisteredException, InvalidMessageException, InvalidVersionException { + + final UUID accountIdentifier = UUID.randomUUID(); + final ServiceIdentifier serviceIdentifier = new AciServiceIdentifier(accountIdentifier); + final byte deviceId = Device.PRIMARY_ID; + final int registrationId = 17; + + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(account.getUuid()).thenReturn(accountIdentifier); + when(account.getIdentifier(IdentityType.ACI)).thenReturn(accountIdentifier); + when(account.isIdentifiedBy(serviceIdentifier)).thenReturn(true); + when(account.getDevices()).thenReturn(List.of(device)); + when(account.getDevice(deviceId)).thenReturn(Optional.of(device)); + when(device.getId()).thenReturn(deviceId); + when(device.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId); + when(device.getApnId()).thenReturn("apns-token"); + when(device.getApnId()).thenReturn("apns-token"); + + final SealedSenderMultiRecipientMessage multiRecipientMessage = + SealedSenderMultiRecipientMessage.parse(MultiRecipientMessageHelper.generateMultiRecipientMessage( + List.of(new TestRecipient(serviceIdentifier, deviceId, registrationId, new byte[48])))); + + final SealedSenderMultiRecipientMessage.Recipient recipient = + multiRecipientMessage.getRecipients().values().iterator().next(); + + when(dynamicMessageDeliveryConfiguration.isReadOnly()).thenReturn(true); + + assertThrows(MessageDeliveryNotAllowedException.class, + () -> messageSender.sendMultiRecipientMessage(multiRecipientMessage, + Map.of(recipient, account), + System.currentTimeMillis(), + false, + false, + false, + null) + .join()); + } + @ParameterizedTest @MethodSource void validateIndividualMessageBundle(final Account destination, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java index cdc81cc9d..4e70bb09d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManagerTest.java @@ -35,6 +35,7 @@ import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager; import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager; +import org.whispersystems.textsecuregcm.controllers.MessageDeliveryNotAllowedException; import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; @@ -284,7 +285,7 @@ public class ChangeNumberManagerTest { @Test void changeNumberRateLimited() - throws MismatchedDevicesException, InterruptedException, MessageTooLargeException, RateLimitExceededException { + throws MismatchedDevicesException, InterruptedException, MessageTooLargeException, RateLimitExceededException, MessageDeliveryNotAllowedException { final String originalNumber = PhoneNumberUtil.getInstance().format( PhoneNumberUtil.getInstance().getExampleNumber("DE"), PhoneNumberUtil.PhoneNumberFormat.E164); @@ -332,7 +333,7 @@ public class ChangeNumberManagerTest { @Test void changeNumberRegistrationLockFailed() - throws MismatchedDevicesException, InterruptedException, MessageTooLargeException, RateLimitExceededException { + throws MismatchedDevicesException, InterruptedException, MessageTooLargeException, RateLimitExceededException, MessageDeliveryNotAllowedException { final String originalNumber = PhoneNumberUtil.getInstance().format( PhoneNumberUtil.getInstance().getExampleNumber("DE"), PhoneNumberUtil.PhoneNumberFormat.E164);