Accommodate gRPC in the `SpamChecker` interface
This commit is contained in:
parent
488e7c4913
commit
7ea0885474
|
@ -103,6 +103,8 @@ import org.whispersystems.textsecuregcm.push.MessageTooLargeException;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
|
import org.whispersystems.textsecuregcm.push.PushNotificationScheduler;
|
||||||
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.MessageType;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.SpamCheckResult;
|
||||||
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
import org.whispersystems.textsecuregcm.spam.SpamChecker;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
@ -303,14 +305,27 @@ public class MessageController {
|
||||||
maybeDestination = source.map(AuthenticatedDevice::getAccount);
|
maybeDestination = source.map(AuthenticatedDevice::getAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SpamChecker.SpamCheckResult spamCheck = spamChecker.checkForSpam(
|
final MessageType messageType;
|
||||||
context, source, maybeDestination, Optional.of(destinationIdentifier));
|
|
||||||
final Optional<byte[]> reportSpamToken;
|
if (isStory) {
|
||||||
switch (spamCheck) {
|
messageType = MessageType.INDIVIDUAL_STORY;
|
||||||
case final SpamChecker.Spam spam: return spam.response();
|
} else if (isSyncMessage) {
|
||||||
case final SpamChecker.NotSpam notSpam: reportSpamToken = notSpam.token();
|
messageType = MessageType.SYNC;
|
||||||
|
} else if (source.isPresent()) {
|
||||||
|
messageType = MessageType.INDIVIDUAL_IDENTIFIED_SENDER;
|
||||||
|
} else {
|
||||||
|
messageType = MessageType.INDIVIDUAL_SEALED_SENDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final SpamCheckResult<Response> spamCheckResult =
|
||||||
|
spamChecker.checkForIndividualRecipientSpamHttp(messageType, context, source, maybeDestination, Optional.of(destinationIdentifier));
|
||||||
|
|
||||||
|
if (spamCheckResult.response().isPresent()) {
|
||||||
|
return spamCheckResult.response().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Optional<byte[]> reportSpamToken = spamCheckResult.token();
|
||||||
|
|
||||||
int totalContentLength = 0;
|
int totalContentLength = 0;
|
||||||
|
|
||||||
for (final IncomingMessage message : messages.messages()) {
|
for (final IncomingMessage message : messages.messages()) {
|
||||||
|
@ -534,9 +549,12 @@ public class MessageController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final SpamChecker.SpamCheckResult spamCheck = spamChecker.checkForSpam(context, Optional.empty(), Optional.empty(), Optional.empty());
|
final SpamCheckResult<Response> spamCheckResult = spamChecker.checkForMultiRecipientSpamHttp(
|
||||||
if (spamCheck instanceof final SpamChecker.Spam spam) {
|
isStory ? MessageType.MULTI_RECIPIENT_STORY : MessageType.MULTI_RECIPIENT_SEALED_SENDER,
|
||||||
return spam.response();
|
context);
|
||||||
|
|
||||||
|
if (spamCheckResult.response().isPresent()) {
|
||||||
|
return spamCheckResult.response().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupSendToken == null && accessKeys == null && !isStory) {
|
if (groupSendToken == null && accessKeys == null && !isStory) {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.spam;
|
||||||
|
|
||||||
|
import io.grpc.Status;
|
||||||
|
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 status The gRPC status for this response. If the status is {@link Status#OK}, then a response object will be
|
||||||
|
* available via {@link #response}. Otherwise, callers should transmit the status as an error to clients.
|
||||||
|
* @param response a response object to send to clients; will be present if {@link #status} is not {@link Status#OK}
|
||||||
|
*
|
||||||
|
* @param <R> the type of response object
|
||||||
|
*/
|
||||||
|
public record GrpcResponse<R>(Status status, Optional<R> response) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <R> the type of response object
|
||||||
|
*/
|
||||||
|
public static <R> GrpcResponse<R> withStatus(final Status status) {
|
||||||
|
return new GrpcResponse<>(status, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <R> the type of response object
|
||||||
|
*/
|
||||||
|
public static <R> GrpcResponse<R> withResponse(final R response) {
|
||||||
|
return new GrpcResponse<>(Status.OK, Optional.of(response));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.spam;
|
||||||
|
|
||||||
|
public enum MessageType {
|
||||||
|
INDIVIDUAL_IDENTIFIED_SENDER,
|
||||||
|
SYNC,
|
||||||
|
INDIVIDUAL_SEALED_SENDER,
|
||||||
|
MULTI_RECIPIENT_SEALED_SENDER,
|
||||||
|
INDIVIDUAL_STORY,
|
||||||
|
MULTI_RECIPIENT_STORY,
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.spam;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a spam check. May contain a response to relay to the caller if a message was identified as potential
|
||||||
|
* spam or a spam reporting token to include in the delivered message.
|
||||||
|
*
|
||||||
|
* @param response a transport-appropriate response to return to the sender if the message was identified as potential
|
||||||
|
* spam, or empty if processing should continue as normal
|
||||||
|
* @param token a spam-reporting token to include in the outbound message, or empty if no token applies to the message
|
||||||
|
*
|
||||||
|
* @param <T> the type of response for messages identified as potential spam
|
||||||
|
*/
|
||||||
|
public record SpamCheckResult<T>(Optional<T> response, Optional<byte[]> token) {
|
||||||
|
}
|
|
@ -5,35 +5,19 @@
|
||||||
package org.whispersystems.textsecuregcm.spam;
|
package org.whispersystems.textsecuregcm.spam;
|
||||||
|
|
||||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||||
import jakarta.ws.rs.core.Response;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import org.signal.chat.messages.SendMessageResponse;
|
||||||
|
import org.signal.chat.messages.SendMultiRecipientMessageResponse;
|
||||||
import org.whispersystems.textsecuregcm.auth.AccountAndAuthenticatedDeviceHolder;
|
import org.whispersystems.textsecuregcm.auth.AccountAndAuthenticatedDeviceHolder;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
|
||||||
public interface SpamChecker {
|
public interface SpamChecker {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A result from the spam checker that is one of:
|
* Determine if a message sent to an individual recipient via HTTP may be spam.
|
||||||
* <ul>
|
|
||||||
* <li>
|
|
||||||
* Message is determined to be spam, and a response is returned
|
|
||||||
* </li>
|
|
||||||
* <li>
|
|
||||||
* Message is not spam, and an optional spam token is returned
|
|
||||||
* </li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
sealed interface SpamCheckResult {}
|
|
||||||
|
|
||||||
record Spam(Response response) implements SpamCheckResult {}
|
|
||||||
|
|
||||||
record NotSpam(Optional<byte[]> token) implements SpamCheckResult {
|
|
||||||
public static final NotSpam EMPTY_TOKEN = new NotSpam(Optional.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a message may be spam
|
|
||||||
*
|
*
|
||||||
* @param requestContext The request context for a message send attempt
|
* @param requestContext The request context for a message send attempt
|
||||||
* @param maybeSource The sender of the message, could be empty if this as message sent with sealed sender
|
* @param maybeSource The sender of the message, could be empty if this as message sent with sealed sender
|
||||||
|
@ -41,13 +25,82 @@ public interface SpamChecker {
|
||||||
* not be retrieved
|
* not be retrieved
|
||||||
* @return A {@link SpamCheckResult}
|
* @return A {@link SpamCheckResult}
|
||||||
*/
|
*/
|
||||||
SpamCheckResult checkForSpam(
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
SpamCheckResult<Response> checkForIndividualRecipientSpamHttp(
|
||||||
|
final MessageType messageType,
|
||||||
final ContainerRequestContext requestContext,
|
final ContainerRequestContext requestContext,
|
||||||
final Optional<? extends AccountAndAuthenticatedDeviceHolder> maybeSource,
|
final Optional<? extends AccountAndAuthenticatedDeviceHolder> maybeSource,
|
||||||
final Optional<Account> maybeDestination,
|
final Optional<Account> maybeDestination,
|
||||||
final Optional<ServiceIdentifier> maybeDestinationIdentifier);
|
final Optional<ServiceIdentifier> maybeDestinationIdentifier);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a message sent to multiple recipients via HTTP may be spam.
|
||||||
|
*
|
||||||
|
* @param requestContext The request context for a message send attempt
|
||||||
|
* @return A {@link SpamCheckResult}
|
||||||
|
*/
|
||||||
|
SpamCheckResult<Response> checkForMultiRecipientSpamHttp(
|
||||||
|
final MessageType messageType,
|
||||||
|
final ContainerRequestContext requestContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a message sent to an individual recipient via gRPC may be spam.
|
||||||
|
*
|
||||||
|
* @param maybeSource The sender of the message, could be empty if this as message sent with sealed sender
|
||||||
|
* @param maybeDestination The destination of the message, could be empty if the destination does not exist or could
|
||||||
|
* not be retrieved
|
||||||
|
* @return A {@link SpamCheckResult}
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
SpamCheckResult<GrpcResponse<SendMessageResponse>> checkForIndividualRecipientSpamGrpc(
|
||||||
|
final MessageType messageType,
|
||||||
|
final Optional<org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice> maybeSource,
|
||||||
|
final Optional<Account> maybeDestination,
|
||||||
|
final Optional<ServiceIdentifier> maybeDestinationIdentifier);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a message sent to multiple recipients via gRPC may be spam.
|
||||||
|
*
|
||||||
|
* @return A {@link SpamCheckResult}
|
||||||
|
*/
|
||||||
|
SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> checkForMultiRecipientSpamGrpc(final MessageType messageType);
|
||||||
|
|
||||||
|
|
||||||
static SpamChecker noop() {
|
static SpamChecker noop() {
|
||||||
return (ignoredContext, ignoredSource, ignoredDestination, ignoredDestinationIdentifier) -> NotSpam.EMPTY_TOKEN;
|
return new SpamChecker() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpamCheckResult<Response> checkForIndividualRecipientSpamHttp(final MessageType messageType,
|
||||||
|
final ContainerRequestContext requestContext,
|
||||||
|
final Optional<? extends AccountAndAuthenticatedDeviceHolder> maybeSource,
|
||||||
|
final Optional<Account> maybeDestination,
|
||||||
|
final Optional<ServiceIdentifier> maybeDestinationIdentifier) {
|
||||||
|
|
||||||
|
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpamCheckResult<Response> checkForMultiRecipientSpamHttp(final MessageType messageType,
|
||||||
|
final ContainerRequestContext requestContext) {
|
||||||
|
|
||||||
|
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpamCheckResult<GrpcResponse<SendMessageResponse>> checkForIndividualRecipientSpamGrpc(final MessageType messageType,
|
||||||
|
final Optional<AuthenticatedDevice> maybeSource,
|
||||||
|
final Optional<Account> maybeDestination,
|
||||||
|
final Optional<ServiceIdentifier> maybeDestinationIdentifier) {
|
||||||
|
|
||||||
|
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> checkForMultiRecipientSpamGrpc(
|
||||||
|
final MessageType messageType) {
|
||||||
|
|
||||||
|
return new SpamCheckResult<>(Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue