diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..f9d8a5ab7
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,11 @@
+# Note that the implmentation of the abusive message filter is private; internal
+# developers will need to override this URL with:
+#
+# ```
+# git config submodule.abusive-message-filter.url PRIVATE_URL
+# ```
+#
+# External developers may safely ignore this submodule.
+[submodule "abusive-message-filter"]
+ path = abusive-message-filter
+ url = REDACTED
diff --git a/abusive-message-filter b/abusive-message-filter
new file mode 160000
index 000000000..817f2e867
--- /dev/null
+++ b/abusive-message-filter
@@ -0,0 +1 @@
+Subproject commit 817f2e867ec1160f793c95f16f50b75c35f2e887
diff --git a/pom.xml b/pom.xml
index 1ddf8211a..5cdfcc2e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -298,6 +298,29 @@
+
+
+ include-abusive-message-filter
+
+
+ abusive-message-filter/pom.xml
+
+
+
+ abusive-message-filter
+
+
+
+
+ exclude-abusive-message-filter
+
+
+ abusive-message-filter/pom.xml
+
+
+
+
+
diff --git a/service/pom.xml b/service/pom.xml
index 306da335d..57eb6c922 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -462,104 +462,112 @@
+
+
+ exclude-abusive-message-filter
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+ true
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.whispersystems.textsecuregcm.WhisperServerService
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.3.0
+
+
+ assembly.xml
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+ org.codehaus.mojo
+ properties-maven-plugin
+ 1.0.0
+
+
+ read-deploy-configuration
+ deploy
+
+ read-project-properties
+
+
+ ${project.basedir}/config/deploy.properties
+
+
+
+
+
+
+ org.signal
+ s3-upload-maven-plugin
+ 1.6-SNAPSHOT
+
+ ${project.build.directory}/${project.build.finalName}-bin.tar.gz
+ ${deploy.bucketName}
+ ${deploy.bucketRegion}
+ ${project.build.finalName}-bin.tar.gz
+
+
+
+ deploy-to-s3
+ deploy
+
+ s3-upload
+
+
+
+
+
+
+
+
${project.parent.artifactId}-${project.version}
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.2.4
-
- true
-
-
- *:*
-
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
-
-
-
-
-
-
- package
-
- shade
-
-
-
-
-
- org.whispersystems.textsecuregcm.WhisperServerService
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
- 3.3.0
-
-
- assembly.xml
-
-
-
-
- make-assembly
- package
-
- single
-
-
-
-
-
-
- org.codehaus.mojo
- properties-maven-plugin
- 1.0.0
-
-
- read-deploy-configuration
- deploy
-
- read-project-properties
-
-
- ${project.basedir}/config/deploy.properties
-
-
-
-
-
-
- org.signal
- s3-upload-maven-plugin
- 1.6-SNAPSHOT
-
- ${project.build.directory}/${project.build.finalName}-bin.tar.gz
- ${deploy.bucketName}
- ${deploy.bucketRegion}
- ${project.build.finalName}-bin.tar.gz
-
-
-
- deploy-to-s3
- deploy
-
- s3-upload
-
-
-
-
-
org.codehaus.mojo
templating-maven-plugin
@@ -573,6 +581,18 @@
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
index 400a2a06f..a7a3a3400 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java
@@ -13,6 +13,7 @@ import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
+import org.whispersystems.textsecuregcm.configuration.AbusiveMessageFilterConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountsDatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.AccountsDynamoDbConfiguration;
@@ -330,6 +331,10 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private ReportMessageConfiguration reportMessage = new ReportMessageConfiguration();
+ @Valid
+ @JsonProperty
+ private AbusiveMessageFilterConfiguration abusiveMessageFilter;
+
private Map transparentDataIndex = new HashMap<>();
public StripeConfiguration getStripe() {
@@ -565,4 +570,8 @@ public class WhisperServerConfiguration extends Configuration {
public ReportMessageConfiguration getReportMessageConfiguration() {
return reportMessage;
}
+
+ public AbusiveMessageFilterConfiguration getAbusiveMessageFilterConfiguration() {
+ return abusiveMessageFilter;
+ }
}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
index 25f31683f..e29785a3e 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java
@@ -44,6 +44,7 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
+import java.util.ServiceLoader;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
@@ -52,6 +53,7 @@ import java.util.concurrent.TimeUnit;
import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
+import javax.ws.rs.container.DynamicFeature;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.ServerProperties;
import org.jdbi.v3.core.Jdbi;
@@ -64,6 +66,8 @@ import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.DispatchManager;
+import org.whispersystems.textsecuregcm.abuse.AbusiveMessageFilter;
+import org.whispersystems.textsecuregcm.abuse.FilterAbusiveMessages;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
@@ -659,6 +663,32 @@ public class WhisperServerService extends Application provisioningEnvironment = new WebSocketEnvironment<>(environment,
webSocketEnvironment.getRequestLog(), 60000);
provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/abuse/AbusiveMessageFilter.java b/service/src/main/java/org/whispersystems/textsecuregcm/abuse/AbusiveMessageFilter.java
new file mode 100644
index 000000000..0a0538769
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/abuse/AbusiveMessageFilter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013-2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.abuse;
+
+import io.dropwizard.lifecycle.Managed;
+import javax.ws.rs.container.ContainerRequestFilter;
+import java.io.IOException;
+
+/**
+ * An abusive message filter is a {@link ContainerRequestFilter} that filters requests to message-sending endpoints to
+ * detect and respond to patterns of abusive behavior.
+ *
+ * Abusive message filters are managed components that are generally loaded dynamically via a
+ * {@link java.util.ServiceLoader}. Their {@link #configure(String)} method will be called prior to be adding to the
+ * server's pool of {@link Managed} objects.
+ *
+ * Abusive message filters must be annotated with {@link FilterAbusiveMessages}, a name binding annotation that
+ * restricts the endpoints to which the filter may apply.
+ */
+public interface AbusiveMessageFilter extends ContainerRequestFilter, Managed {
+
+ /**
+ * Configures this abusive message filter. This method will be called before the filter is added to the server's pool
+ * of managed objects and before the server processes any requests.
+ *
+ * @param environmentName the name of the environment in which this filter is running (e.g. "staging" or "production")
+ * @throws IOException if the filter could not read its configuration source for any reason
+ */
+ void configure(String environmentName) throws IOException;
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/abuse/FilterAbusiveMessages.java b/service/src/main/java/org/whispersystems/textsecuregcm/abuse/FilterAbusiveMessages.java
new file mode 100644
index 000000000..09c7f556f
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/abuse/FilterAbusiveMessages.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013-2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.abuse;
+
+import javax.ws.rs.NameBinding;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A name-binding annotation that associates {@link AbusiveMessageFilter}s with resource methods.
+ */
+@NameBinding
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface FilterAbusiveMessages {
+}
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AbusiveMessageFilterConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AbusiveMessageFilterConfiguration.java
new file mode 100644
index 000000000..c815aca1b
--- /dev/null
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/AbusiveMessageFilterConfiguration.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013-2021 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.configuration;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.validation.constraints.NotBlank;
+
+public class AbusiveMessageFilterConfiguration {
+
+ @JsonProperty
+ @NotBlank
+ private final String environment;
+
+ @JsonCreator
+ public AbusiveMessageFilterConfiguration(@JsonProperty("environment") final String environment) {
+ this.environment = environment;
+ }
+
+ public String getEnvironment() {
+ return environment;
+ }
+}
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 df1762b12..2a032b371 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java
@@ -58,6 +58,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.whispersystems.textsecuregcm.abuse.FilterAbusiveMessages;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.CombinedUnidentifiedSenderAccessKeys;
@@ -163,6 +164,7 @@ public class MessageController {
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
+ @FilterAbusiveMessages
public Response sendMessage(@Auth Optional source,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional accessKey,
@HeaderParam("User-Agent") String userAgent,
@@ -285,6 +287,7 @@ public class MessageController {
@PUT
@Consumes(MultiRecipientMessageProvider.MEDIA_TYPE)
@Produces(MediaType.APPLICATION_JSON)
+ @FilterAbusiveMessages
public Response sendMultiRecipientMessage(
@HeaderParam(OptionalAccess.UNIDENTIFIED) CombinedUnidentifiedSenderAccessKeys accessKeys,
@HeaderParam("User-Agent") String userAgent,
diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java
index 4fb9217dd..cb22d92ee 100644
--- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java
+++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DynamicConfigurationManager.java
@@ -59,7 +59,7 @@ public class DynamicConfigurationManager {
}
@VisibleForTesting
- public DynamicConfigurationManager(AppConfigClient appConfigClient, String application, String environment,
+ DynamicConfigurationManager(AppConfigClient appConfigClient, String application, String environment,
String configurationName, String clientId, Class configurationClass) {
this.appConfigClient = appConfigClient;
this.application = application;