From ca2f7d2eed0259bda9dcc6e592b36252fb08a5a8 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Wed, 10 Mar 2021 17:52:40 -0500 Subject: [PATCH] Parse locale strings when sending voice verification codes. --- .../controllers/AccountController.java | 9 +++- .../textsecuregcm/sms/SmsSender.java | 5 +- .../textsecuregcm/sms/TwilioSmsSender.java | 15 ++++-- .../controllers/AccountControllerTest.java | 52 +++++++++++++++++++ .../tests/sms/TwilioSmsSenderTest.java | 9 ++-- 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 577dd2dc2..73e6f6ebc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -16,6 +16,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -184,7 +185,7 @@ public class AccountController { @PathParam("number") String number, @HeaderParam("X-Forwarded-For") String forwardedFor, @HeaderParam("User-Agent") List userAgent, - @HeaderParam("Accept-Language") Optional locale, + @HeaderParam("Accept-Language") Optional acceptLanguage, @QueryParam("client") Optional client, @QueryParam("captcha") Optional captcha, @QueryParam("challenge") Optional pushChallenge) @@ -247,7 +248,11 @@ public class AccountController { } else if (transport.equals("sms")) { smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay()); } else if (transport.equals("voice")) { - smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), locale); + final Optional maybeLocale = acceptLanguage.map(Locale.LanguageRange::parse) + .flatMap(ranges -> ranges.stream().findFirst()) + .map(range -> Locale.forLanguageTag(range.getRange())); + + smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), maybeLocale); } metricRegistry.meter(name(AccountController.class, "create", Util.getCountryCode(number))).mark(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java index 9e2836361..43dd9803c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java @@ -5,6 +5,7 @@ package org.whispersystems.textsecuregcm.sms; +import java.util.Locale; import java.util.Optional; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -24,7 +25,7 @@ public class SmsSender { twilioSender.deliverSmsVerification(destination, clientType, verificationCode); } - public void deliverVoxVerification(String destination, String verificationCode, Optional locale) { - twilioSender.deliverVoxVerification(destination, verificationCode, locale); + public void deliverVoxVerification(String destination, String verificationCode, Optional maybeLocale) { + twilioSender.deliverVoxVerification(destination, verificationCode, maybeLocale); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java index 295dabae3..d61f5a2cd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java @@ -30,6 +30,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; @@ -143,11 +144,19 @@ public class TwilioSmsSender { } } - public CompletableFuture deliverVoxVerification(String destination, String verificationCode, Optional locale) { + public CompletableFuture deliverVoxVerification(String destination, String verificationCode, Optional maybeLocale) { String url = "https://" + localDomain + "/v1/voice/description/" + verificationCode; - if (locale.isPresent()) { - url += "?l=" + locale.get(); + if (maybeLocale.isPresent()) { + final String localeString = maybeLocale.map(locale -> { + if (StringUtils.isNotBlank(locale.getCountry())) { + return locale.getLanguage().toLowerCase() + "-" + locale.getCountry().toUpperCase(); + } else { + return locale.getLanguage().toLowerCase(); + } + }).get(); + + url += "?l=" + localeString; } Map requestParameters = new HashMap<>(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 275df6a98..ae2ff82e5 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -28,6 +28,7 @@ import java.security.SecureRandom; import java.time.Duration; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -163,6 +164,7 @@ public class AccountControllerTest { when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getVoiceDestinationDailyLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); when(rateLimiters.getPinLimiter()).thenReturn(pinLimiter); when(rateLimiters.getSmsVoiceIpLimiter()).thenReturn(smsVoiceIpLimiter); @@ -294,6 +296,56 @@ public class AccountControllerTest { verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); } + @Test + public void testSendCodeVoiceNoLocale() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/voice/code/%s", SENDER)) + .queryParam("challenge", "1234-push") + .request() + .header("X-Forwarded-For", NICE_HOST) + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Optional.empty())); + verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); + } + + @Test + public void testSendCodeVoiceSingleLocale() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/voice/code/%s", SENDER)) + .queryParam("challenge", "1234-push") + .request() + .header("Accept-Language", "pt-BR") + .header("X-Forwarded-For", NICE_HOST) + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Optional.of(Locale.forLanguageTag("pt-BR")))); + verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); + } + + @Test + public void testSendCodeVoiceMultipleLocales() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/voice/code/%s", SENDER)) + .queryParam("challenge", "1234-push") + .request() + .header("Accept-Language", "en-US;q=1, ar-US;q=0.9, fa-US;q=0.8, zh-Hans-US;q=0.7, ru-RU;q=0.6, zh-Hant-US;q=0.5") + .header("X-Forwarded-For", NICE_HOST) + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Optional.of(Locale.forLanguageTag("en-US")))); + verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); + } + @Test public void testSendCodeWithValidPreauth() throws Exception { Response response = diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/TwilioSmsSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/TwilioSmsSenderTest.java index 4ba87bb2c..1e316804e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/TwilioSmsSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/TwilioSmsSenderTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.*; import com.github.tomakehurst.wiremock.junit.WireMockRule; import java.util.List; +import java.util.Locale; import java.util.Optional; import javax.annotation.Nonnull; import org.junit.Before; @@ -135,13 +136,13 @@ public class TwilioSmsSenderTest { TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration, dynamicConfigurationManager); - boolean success = sender.deliverVoxVerification("+14153333333", "123-456", Optional.of("en_US")).join(); + boolean success = sender.deliverVoxVerification("+14153333333", "123-456", Optional.of(Locale.US)).join(); assertThat(success).isTrue(); verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json")) .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den_US"))); + .withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den-US"))); } @Test @@ -178,13 +179,13 @@ public class TwilioSmsSenderTest { TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration, dynamicConfigurationManager); - boolean success = sender.deliverVoxVerification("+14153333333", "123-456", Optional.of("en_US")).join(); + boolean success = sender.deliverVoxVerification("+14153333333", "123-456", Optional.of(Locale.US)).join(); assertThat(success).isFalse(); verify(3, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json")) .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den_US"))); + .withRequestBody(matching("To=%2B14153333333&From=%2B1415(1111111|2222222)&Url=https%3A%2F%2Ftest.com%2Fv1%2Fvoice%2Fdescription%2F123-456%3Fl%3Den-US"))); }