From 1461bcc2c25b396e824cb2737147abcdd3d1088d Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Wed, 10 Nov 2021 16:55:40 -0500 Subject: [PATCH] Correct envelope types for certain iOS builds --- .../controllers/MessageController.java | 40 +++++++++++++++++-- .../controllers/MessageControllerTest.java | 32 +++++++++++++++ ...urrent_message_single_device_bad_type.json | 8 ++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 service/src/test/resources/fixtures/current_message_single_device_bad_type.json diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java index 6fb8249f6..d250c7bdb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -14,6 +14,7 @@ import com.codahale.metrics.Timer; import com.codahale.metrics.annotation.Timed; import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.ByteString; +import com.vdurmont.semver4j.Semver; import io.dropwizard.auth.Auth; import io.dropwizard.util.DataSize; import io.micrometer.core.instrument.Counter; @@ -42,6 +43,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nonnull; import javax.validation.Valid; +import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -96,7 +98,9 @@ import org.whispersystems.textsecuregcm.storage.ReportMessageManager; import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.Util; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException; +import org.whispersystems.textsecuregcm.util.ua.UserAgent; import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil; import org.whispersystems.textsecuregcm.websocket.WebSocketConnection; @@ -124,6 +128,12 @@ public class MessageController { private final ReportMessageManager reportMessageManager; private final ExecutorService multiRecipientMessageExecutor; + @VisibleForTesting + static final Semver FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE = new Semver("5.22.0.32"); + + @VisibleForTesting + static final Semver IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE = new Semver("5.25.0.0"); + private static final String LEGACY_MESSAGE_SENT_COUNTER = name(MessageController.class, "legacyMessageSent"); private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages"); private static final String CONTENT_SIZE_DISTRIBUTION_NAME = name(MessageController.class, "messageContentSize"); @@ -269,7 +279,7 @@ public class MessageController { if (destinationDevice.isPresent()) { Metrics.counter(SENT_MESSAGE_COUNTER_NAME, tags).increment(); sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), messages.isOnline(), - incomingMessage); + incomingMessage, userAgent); } } @@ -514,14 +524,38 @@ public class MessageController { Device destinationDevice, long timestamp, boolean online, - IncomingMessage incomingMessage) + IncomingMessage incomingMessage, + String userAgentString) throws NoSuchUserException { try (final Timer.Context ignored = sendMessageInternalTimer.time()) { Optional messageBody = getMessageBody(incomingMessage); Optional messageContent = getMessageContent(incomingMessage); Envelope.Builder messageBuilder = Envelope.newBuilder(); - messageBuilder.setType(Envelope.Type.forNumber(incomingMessage.getType())) + int envelopeTypeNumber = incomingMessage.getType(); + + // Some versions of the iOS app incorrectly use the reserved envelope type 7 for PLAINTEXT_CONTENT instead of type + // 8. This check can be removed safely after 2022-03-01. + if (envelopeTypeNumber == 7) { + try { + final UserAgent userAgent = UserAgentUtil.parseUserAgentString(userAgentString); + if (userAgent.getPlatform() == ClientPlatform.IOS && + userAgent.getVersion().isGreaterThanOrEqualTo(FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE) && + userAgent.getVersion().isLowerThan(IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE)) { + envelopeTypeNumber = Type.PLAINTEXT_CONTENT.getNumber(); + } + } catch (final UnrecognizedUserAgentException ignored2) { + } + } + + final Envelope.Type envelopeType = Envelope.Type.forNumber(envelopeTypeNumber); + + if (envelopeType == null) { + logger.warn("Received bad envelope type {} from {}", incomingMessage.getType(), userAgentString); + throw new BadRequestException(); + } + + messageBuilder.setType(envelopeType) .setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp) .setServerTimestamp(System.currentTimeMillis()); 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 751d02685..edc0646a4 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/MessageControllerTest.java @@ -218,6 +218,38 @@ class MessageControllerTest { assertTrue(captor.getValue().hasSourceDevice()); } + @ParameterizedTest + @MethodSource + void testSingleDeviceCurrentBadType(final String userAgentString, final boolean expectAcceptMessage) throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/messages/%s", SINGLE_DEVICE_UUID)) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .header("User-Agent", userAgentString) + .put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device_bad_type.json"), IncomingMessageList.class), + MediaType.APPLICATION_JSON_TYPE)); + + if (expectAcceptMessage) { + assertEquals(200, response.getStatus()); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Envelope.class); + verify(messageSender).sendMessage(any(Account.class), any(Device.class), captor.capture(), eq(false)); + } else { + assertEquals(400, response.getStatus()); + verify(messageSender, never()).sendMessage(any(), any(), any(), anyBoolean()); + } + } + + private static Stream testSingleDeviceCurrentBadType() { + return Stream.of( + Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE), true), + Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.FIRST_IOS_VERSION_WITH_INCORRECT_ENVELOPE_TYPE.nextPatch()), true), + Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE.withIncMinor(-1)), true), + Arguments.of(String.format("Signal-iOS/%s iOS/14.2", MessageController.IOS_VERSION_WITH_FIXED_ENVELOPE_TYPE), false) + ); + } + @Test void testNullMessageInList() throws Exception { Response response = diff --git a/service/src/test/resources/fixtures/current_message_single_device_bad_type.json b/service/src/test/resources/fixtures/current_message_single_device_bad_type.json new file mode 100644 index 000000000..f508362db --- /dev/null +++ b/service/src/test/resources/fixtures/current_message_single_device_bad_type.json @@ -0,0 +1,8 @@ +{ + "messages" : [{ + "type" : 7, + "destinationDeviceId" : 1, + "body" : "Zm9vYmFyego", + "timestamp" : 1234 + }] +}