mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 04:38:04 +01:00
Centralize message length validation
This commit is contained in:
committed by
Jon Chambers
parent
faef614d80
commit
50f681ffe8
@@ -7,11 +7,8 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
@@ -102,6 +99,7 @@ import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
|
||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||
@@ -155,10 +153,7 @@ public class MessageController {
|
||||
|
||||
private static final CompletableFuture<?>[] EMPTY_FUTURE_ARRAY = new CompletableFuture<?>[0];
|
||||
|
||||
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "rejectOversizeMessage");
|
||||
private static final String LARGE_BUT_NOT_OVERSIZE_MESSAGE_COUNTER = name(MessageController.class, "largeMessage");
|
||||
private static final String SENT_MESSAGE_COUNTER_NAME = name(MessageController.class, "sentMessages");
|
||||
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = MetricsUtil.name(MessageController.class, "messageContentSize");
|
||||
private static final String OUTGOING_MESSAGE_LIST_SIZE_BYTES_DISTRIBUTION_NAME = name(MessageController.class, "outgoingMessageListSizeBytes");
|
||||
private static final String RATE_LIMITED_MESSAGE_COUNTER_NAME = name(MessageController.class, "rateLimitedMessage");
|
||||
|
||||
@@ -184,10 +179,6 @@ public class MessageController {
|
||||
private static final String ENDPOINT_TYPE_SINGLE = "single";
|
||||
private static final String ENDPOINT_TYPE_MULTI = "multi";
|
||||
|
||||
@VisibleForTesting
|
||||
static final int MAX_MESSAGE_SIZE = (int) DataSize.kibibytes(256).toBytes();
|
||||
private static final long LARGE_MESSAGE_SIZE = DataSize.kibibytes(8).toBytes();
|
||||
|
||||
// The Signal desktop client (really, JavaScript in general) can handle message timestamps at most 100,000,000 days
|
||||
// past the epoch; please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date
|
||||
// for additional details.
|
||||
@@ -325,7 +316,11 @@ public class MessageController {
|
||||
for (final IncomingMessage message : messages.messages()) {
|
||||
final int contentLength = message.content() != null ? message.content().length : 0;
|
||||
|
||||
validateContentLength(contentLength, false, isSyncMessage, isStory, userAgent);
|
||||
try {
|
||||
MessageSender.validateContentLength(contentLength, false, isSyncMessage, isStory, userAgent);
|
||||
} catch (final MessageTooLargeException e) {
|
||||
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
|
||||
}
|
||||
|
||||
totalContentLength += contentLength;
|
||||
}
|
||||
@@ -513,8 +508,13 @@ public class MessageController {
|
||||
}
|
||||
|
||||
// Verify that the message isn't too large before performing more expensive validations
|
||||
multiRecipientMessage.getRecipients().values().forEach(recipient ->
|
||||
validateContentLength(multiRecipientMessage.messageSizeForRecipient(recipient), true, false, isStory, userAgent));
|
||||
multiRecipientMessage.getRecipients().values().forEach(recipient -> {
|
||||
try {
|
||||
MessageSender.validateContentLength(multiRecipientMessage.messageSizeForRecipient(recipient), true, false, isStory, userAgent);
|
||||
} catch (final MessageTooLargeException e) {
|
||||
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
|
||||
}
|
||||
});
|
||||
|
||||
// Check that the request is well-formed and doesn't contain repeated entries for the same device for the same
|
||||
// recipient
|
||||
@@ -920,38 +920,4 @@ public class MessageController {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateContentLength(final int contentLength,
|
||||
final boolean isMultiRecipientMessage,
|
||||
final boolean isSyncMessage,
|
||||
final boolean isStory,
|
||||
final String userAgent) {
|
||||
|
||||
final boolean oversize = contentLength > MAX_MESSAGE_SIZE;
|
||||
|
||||
DistributionSummary.builder(CONTENT_SIZE_DISTRIBUTION_NAME)
|
||||
.tags(Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of("oversize", String.valueOf(oversize)),
|
||||
Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage)),
|
||||
Tag.of("syncMessage", String.valueOf(isSyncMessage)),
|
||||
Tag.of("story", String.valueOf(isStory))))
|
||||
.publishPercentileHistogram(true)
|
||||
.register(Metrics.globalRegistry)
|
||||
.record(contentLength);
|
||||
|
||||
if (oversize) {
|
||||
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage)),
|
||||
Tag.of("syncMessage", String.valueOf(isSyncMessage)),
|
||||
Tag.of("story", String.valueOf(isStory))))
|
||||
.increment();
|
||||
throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);
|
||||
}
|
||||
if (contentLength > LARGE_MESSAGE_SIZE) {
|
||||
Metrics.counter(
|
||||
LARGE_BUT_NOT_OVERSIZE_MESSAGE_COUNTER,
|
||||
Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage))))
|
||||
.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,18 @@ import static com.codahale.metrics.MetricRegistry.name;
|
||||
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.signal.libsignal.protocol.SealedSenderMultiRecipientMessage;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||
@@ -34,6 +41,11 @@ public class MessageSender {
|
||||
private final MessagesManager messagesManager;
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
|
||||
// Note that these names deliberately reference `MessageController` for metric continuity
|
||||
private static final String REJECT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "rejectOversizeMessage");
|
||||
private static final String LARGE_BUT_NOT_OVERSIZE_MESSAGE_COUNTER_NAME = name(MessageController.class, "largeMessage");
|
||||
private static final String CONTENT_SIZE_DISTRIBUTION_NAME = MetricsUtil.name(MessageController.class, "messageContentSize");
|
||||
|
||||
private static final String SEND_COUNTER_NAME = name(MessageSender.class, "sendMessage");
|
||||
private static final String CHANNEL_TAG_NAME = "channel";
|
||||
private static final String EPHEMERAL_TAG_NAME = "ephemeral";
|
||||
@@ -42,6 +54,10 @@ public class MessageSender {
|
||||
private static final String STORY_TAG_NAME = "story";
|
||||
private static final String SEALED_SENDER_TAG_NAME = "sealedSender";
|
||||
|
||||
@VisibleForTesting
|
||||
public static final int MAX_MESSAGE_SIZE = (int) DataSize.kibibytes(256).toBytes();
|
||||
private static final long LARGE_MESSAGE_SIZE = DataSize.kibibytes(8).toBytes();
|
||||
|
||||
public MessageSender(final MessagesManager messagesManager, final PushNotificationManager pushNotificationManager) {
|
||||
this.messagesManager = messagesManager;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
@@ -137,4 +153,40 @@ public class MessageSender {
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateContentLength(final int contentLength,
|
||||
final boolean isMultiRecipientMessage,
|
||||
final boolean isSyncMessage,
|
||||
final boolean isStory,
|
||||
final String userAgent) throws MessageTooLargeException {
|
||||
|
||||
final boolean oversize = contentLength > MAX_MESSAGE_SIZE;
|
||||
|
||||
DistributionSummary.builder(CONTENT_SIZE_DISTRIBUTION_NAME)
|
||||
.tags(Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of("oversize", String.valueOf(oversize)),
|
||||
Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage)),
|
||||
Tag.of("syncMessage", String.valueOf(isSyncMessage)),
|
||||
Tag.of("story", String.valueOf(isStory))))
|
||||
.publishPercentileHistogram(true)
|
||||
.register(Metrics.globalRegistry)
|
||||
.record(contentLength);
|
||||
|
||||
if (oversize) {
|
||||
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage)),
|
||||
Tag.of("syncMessage", String.valueOf(isSyncMessage)),
|
||||
Tag.of("story", String.valueOf(isStory))))
|
||||
.increment();
|
||||
|
||||
throw new MessageTooLargeException();
|
||||
}
|
||||
|
||||
if (contentLength > LARGE_MESSAGE_SIZE) {
|
||||
Metrics.counter(
|
||||
LARGE_BUT_NOT_OVERSIZE_MESSAGE_COUNTER_NAME,
|
||||
Tags.of(UserAgentTagUtil.getPlatformTag(userAgent), Tag.of("multiRecipientMessage", String.valueOf(isMultiRecipientMessage))))
|
||||
.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.push;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.NoStackTraceException;
|
||||
|
||||
public class MessageTooLargeException extends NoStackTraceException {
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
/**
|
||||
* An abstract base class for exceptions that do not include a stack trace. Stackless exceptions are generally intended
|
||||
* for internal error-handling cases where the error will never be logged or otherwise reported.
|
||||
*/
|
||||
public abstract class NoStackTraceException extends Exception {
|
||||
|
||||
public NoStackTraceException() {
|
||||
super(null, null, true, false);
|
||||
}
|
||||
|
||||
public NoStackTraceException(final String message) {
|
||||
super(message, null, true, false);
|
||||
}
|
||||
|
||||
public NoStackTraceException(final String message, final Throwable cause) {
|
||||
super(message, cause, true, false);
|
||||
}
|
||||
|
||||
public NoStackTraceException(final Throwable cause) {
|
||||
super(null, cause, true, false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user