diff --git a/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java index 040e723ad..9ef955a58 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java @@ -32,6 +32,9 @@ public class RateLimitsConfiguration { @JsonProperty private RateLimitConfiguration smsVoiceIp = new RateLimitConfiguration(1000, 1000); + @JsonProperty + private RateLimitConfiguration smsVoicePrefix = new RateLimitConfiguration(1000, 1000); + @JsonProperty private RateLimitConfiguration verifyNumber = new RateLimitConfiguration(2, 2); @@ -102,6 +105,10 @@ public class RateLimitsConfiguration { return smsVoiceIp; } + public RateLimitConfiguration getSmsVoicePrefix() { + return smsVoicePrefix; + } + public RateLimitConfiguration getVerifyNumber() { return verifyNumber; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 0da089e93..4cee242f7 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -80,13 +80,16 @@ import io.dropwizard.auth.Auth; @Path("/v1/accounts") public class AccountController { - private final Logger logger = LoggerFactory.getLogger(AccountController.class); - private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); - private final Meter newUserMeter = metricRegistry.meter(name(AccountController.class, "brand_new_user" )); - private final Meter blockedHostMeter = metricRegistry.meter(name(AccountController.class, "blocked_host" )); - private final Meter filteredHostMeter = metricRegistry.meter(name(AccountController.class, "filtered_host" )); - private final Meter captchaSuccessMeter = metricRegistry.meter(name(AccountController.class, "captcha_success")); - private final Meter captchaFailureMeter = metricRegistry.meter(name(AccountController.class, "captcha_failure")); + private final Logger logger = LoggerFactory.getLogger(AccountController.class); + private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + private final Meter newUserMeter = metricRegistry.meter(name(AccountController.class, "brand_new_user" )); + private final Meter blockedHostMeter = metricRegistry.meter(name(AccountController.class, "blocked_host" )); + private final Meter filteredHostMeter = metricRegistry.meter(name(AccountController.class, "filtered_host" )); + private final Meter rateLimitedHostMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_host" )); + private final Meter rateLimitedPrefixMeter = metricRegistry.meter(name(AccountController.class, "rate_limited_prefix")); + private final Meter captchaSuccessMeter = metricRegistry.meter(name(AccountController.class, "captcha_success" )); + private final Meter captchaFailureMeter = metricRegistry.meter(name(AccountController.class, "captcha_failure" )); + private final PendingAccountsManager pendingAccounts; private final AccountsManager accounts; @@ -420,10 +423,19 @@ public class AccountController { rateLimiters.getSmsVoiceIpLimiter().validate(requester); } catch (RateLimitExceededException e) { logger.info("Rate limited exceeded: " + transport + ", " + number + ", " + requester + " (" + forwardedFor + ")"); + rateLimitedPrefixMeter.mark(); return true; } } + try { + rateLimiters.getSmsVoicePrefixLimiter().validate(Util.getNumberPrefix(number)); + } catch (RateLimitExceededException e) { + logger.info("Prefix rate limit exceeded: " + transport + ", " + number + ", (" + String.join(", ", requesters) + ")"); + rateLimitedPrefixMeter.mark(); + return true; + } + return false; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java index e7999834d..2f68531ac 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java +++ b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java @@ -26,6 +26,7 @@ public class RateLimiters { private final RateLimiter voiceDestinationLimiter; private final RateLimiter voiceDestinationDailyLimiter; private final RateLimiter smsVoiceIpLimiter; + private final RateLimiter smsVoicePrefixLimiter; private final RateLimiter verifyLimiter; private final RateLimiter pinLimiter; @@ -58,6 +59,10 @@ public class RateLimiters { config.getSmsVoiceIp().getBucketSize(), config.getSmsVoiceIp().getLeakRatePerMinute()); + this.smsVoicePrefixLimiter = new RateLimiter(cacheClient, "smsVoicePrefix", + config.getSmsVoicePrefix().getBucketSize(), + config.getSmsVoicePrefix().getLeakRatePerMinute()); + this.verifyLimiter = new RateLimiter(cacheClient, "verify", config.getVerifyNumber().getBucketSize(), config.getVerifyNumber().getLeakRatePerMinute()); @@ -131,6 +136,10 @@ public class RateLimiters { return smsVoiceIpLimiter; } + public RateLimiter getSmsVoicePrefixLimiter() { + return smsVoicePrefixLimiter; + } + public RateLimiter getVoiceDestinationLimiter() { return voiceDestinationLimiter; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/Util.java b/src/main/java/org/whispersystems/textsecuregcm/util/Util.java index 8164a98a5..5ac8e6c33 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/util/Util.java +++ b/src/main/java/org/whispersystems/textsecuregcm/util/Util.java @@ -60,6 +60,14 @@ public class Util { else return "0"; } + public static String getNumberPrefix(String number) { + String countryCode = getCountryCode(number); + int remaining = number.length() - (1 + countryCode.length()); + int prefixLength = Math.min(4, remaining); + + return number.substring(0, 1 + countryCode.length() + prefixLength); + } + public static String encodeFormParams(Map params) { try { StringBuffer buffer = new StringBuffer(); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 9ed1b184a..33b0d8325 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -63,6 +63,7 @@ public class AccountControllerTest { private RateLimiter rateLimiter = mock(RateLimiter.class ); private RateLimiter pinLimiter = mock(RateLimiter.class ); private RateLimiter smsVoiceIpLimiter = mock(RateLimiter.class ); + private RateLimiter smsVoicePrefixLimiter = mock(RateLimiter.class); private SmsSender smsSender = mock(SmsSender.class ); private DirectoryQueue directoryQueue = mock(DirectoryQueue.class); private MessagesManager storedMessages = mock(MessagesManager.class ); @@ -98,6 +99,7 @@ public class AccountControllerTest { when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); when(rateLimiters.getPinLimiter()).thenReturn(pinLimiter); when(rateLimiters.getSmsVoiceIpLimiter()).thenReturn(smsVoiceIpLimiter); + when(rateLimiters.getSmsVoicePrefixLimiter()).thenReturn(smsVoicePrefixLimiter); when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis()); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/util/NumberPrefixTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/util/NumberPrefixTest.java new file mode 100644 index 000000000..e5ff2b979 --- /dev/null +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/util/NumberPrefixTest.java @@ -0,0 +1,18 @@ +package org.whispersystems.textsecuregcm.tests.util; + +import org.junit.Test; +import org.whispersystems.textsecuregcm.util.Util; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class NumberPrefixTest { + + @Test + public void testPrefixes() { + assertThat(Util.getNumberPrefix("+14151234567")).isEqualTo("+14151"); + assertThat(Util.getNumberPrefix("+22587654321")).isEqualTo("+2258765"); + assertThat(Util.getNumberPrefix("+298654321")).isEqualTo("+2986543"); + assertThat(Util.getNumberPrefix("+12")).isEqualTo("+12"); + } + +}