From 760462f8fbadf6fa6204d6d651c7584e93c08921 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Thu, 6 May 2021 13:16:34 -0500 Subject: [PATCH] Add configuration for regional SMS verification text --- service/config/sample.yml | 15 ++-- .../configuration/TwilioConfiguration.java | 63 ++++------------- .../TwilioVerificationTextConfiguration.java | 70 +++++++++++++++++++ .../textsecuregcm/sms/TwilioSmsSender.java | 35 +++++----- .../tests/sms/TwilioSmsSenderTest.java | 51 ++++++++++++-- 5 files changed, 158 insertions(+), 76 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java diff --git a/service/config/sample.yml b/service/config/sample.yml index dee867d48..453c439ce 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -5,11 +5,16 @@ twilio: # Twilio gateway configuration messagingServiceSid: # Twilio SID for the message service to use for non-NANPA. verifyServiceSid: # Twilio SID for a Verify service localDomain: # Domain Twilio can connect back to for calls. Should be domain of your service. - iosVerificationText: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1. - androidNgVerificationText: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1. - android202001VerificationText: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1. - android202103VerificationText: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1. - genericVerificationText: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1. + defaultClientVerificationTexts: + ios: # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1. + androidNg: # Text to use for the verification message on android-ng client types. Will be passed to String.format with the verification code as argument 1. + android202001: # Text to use for the verification message on android-2020-01 client types. Will be passed to String.format with the verification code as argument 1. + android202103: # Text to use for the verification message on android-2021-03 client types. Will be passed to String.format with the verification code as argument 1. + generic: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1. + regionalClientVerificationTexts: # Map of country codes to custom texts + 999: # example country code + ios: + # … all keys from defaultClientVerificationTexts are required androidAppHash: # Hash appended to Android verifyServiceFriendlyName: # Service name used in template. Requires Twilio account rep to enable diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java index 1677eb5d4..cbc614bc5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java @@ -5,6 +5,8 @@ package org.whispersystems.textsecuregcm.configuration; import com.google.common.annotations.VisibleForTesting; +import java.util.Collections; +import java.util.Map; import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @@ -37,20 +39,11 @@ public class TwilioConfiguration { @Valid private RetryConfiguration retry = new RetryConfiguration(); - @NotEmpty - private String iosVerificationText; + @Valid + private TwilioVerificationTextConfiguration defaultClientVerificationTexts; - @NotEmpty - private String androidNgVerificationText; - - @NotEmpty - private String android202001VerificationText; - - @NotEmpty - private String android202103VerificationText; - - @NotEmpty - private String genericVerificationText; + @Valid + private Map regionalClientVerificationTexts = Collections.emptyMap(); @NotEmpty private String androidAppHash; @@ -129,49 +122,23 @@ public class TwilioConfiguration { this.retry = retry; } - public String getIosVerificationText() { - return iosVerificationText; + public TwilioVerificationTextConfiguration getDefaultClientVerificationTexts() { + return defaultClientVerificationTexts; } @VisibleForTesting - public void setIosVerificationText(String iosVerificationText) { - this.iosVerificationText = iosVerificationText; + public void setDefaultClientVerificationTexts(TwilioVerificationTextConfiguration defaultClientVerificationTexts) { + this.defaultClientVerificationTexts = defaultClientVerificationTexts; } - public String getAndroidNgVerificationText() { - return androidNgVerificationText; + + public Map getRegionalClientVerificationTexts() { + return regionalClientVerificationTexts; } @VisibleForTesting - public void setAndroidNgVerificationText(String androidNgVerificationText) { - this.androidNgVerificationText = androidNgVerificationText; - } - - public String getAndroid202001VerificationText() { - return android202001VerificationText; - } - - @VisibleForTesting - public void setAndroid202001VerificationText(String android202001VerificationText) { - this.android202001VerificationText = android202001VerificationText; - } - - public String getAndroid202103VerificationText() { - return android202103VerificationText; - } - - @VisibleForTesting - public void setAndroid202103VerificationText(String android202103VerificationText) { - this.android202103VerificationText = android202103VerificationText; - } - - public String getGenericVerificationText() { - return genericVerificationText; - } - - @VisibleForTesting - public void setGenericVerificationText(String genericVerificationText) { - this.genericVerificationText = genericVerificationText; + public void setRegionalClientVerificationTexts(final Map regionalClientVerificationTexts) { + this.regionalClientVerificationTexts = regionalClientVerificationTexts; } public String getAndroidAppHash() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java new file mode 100644 index 000000000..693713039 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java @@ -0,0 +1,70 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import javax.validation.constraints.NotEmpty; + +public class TwilioVerificationTextConfiguration { + + @JsonProperty + @NotEmpty + private String ios; + + @JsonProperty + @NotEmpty + private String androidNg; + + @JsonProperty + @NotEmpty + private String android202001; + + @JsonProperty + @NotEmpty + private String android202103; + + @JsonProperty + @NotEmpty + private String generic; + + public String getIosText() { + return ios; + } + + public void setIosText(String ios) { + this.ios = ios; + } + + public String getAndroidNgText() { + return androidNg; + } + + public void setAndroidNgText(final String androidNg) { + this.androidNg = androidNg; + } + + public String getAndroid202001Text() { + return android202001; + } + + public void setAndroid202001Text(final String android202001) { + this.android202001 = android202001; + } + + public String getAndroid202103Text() { + return android202103; + } + + public void setAndroid202103Text(final String android202103) { + this.android202103 = android202103; + } + + public String getGenericText() { + return generic; + } + + public void setGenericText(final String generic) { + this.generic = generic; + } +} 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 c32c81595..a9244f99a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java @@ -37,6 +37,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; +import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration; import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; @@ -64,11 +65,9 @@ public class TwilioSmsSender { private final String nanpaMessagingServiceSid; private final String localDomain; private final Random random; - private final String androidNgVerificationText; - private final String android202001VerificationText; - private final String android202103VerificationText; - private final String iosVerificationText; - private final String genericVerificationText; + + private final TwilioVerificationTextConfiguration defaultClientVerificationTexts; + private final Map regionalClientVerificationTexts; private final FaultTolerantHttpClient httpClient; private final URI smsUri; @@ -88,11 +87,6 @@ public class TwilioSmsSender { this.messagingServiceSid = twilioConfiguration.getMessagingServiceSid(); this.nanpaMessagingServiceSid = twilioConfiguration.getNanpaMessagingServiceSid(); this.random = new Random(System.currentTimeMillis()); - this.androidNgVerificationText = twilioConfiguration.getAndroidNgVerificationText(); - this.android202001VerificationText = twilioConfiguration.getAndroid202001VerificationText(); - this.android202103VerificationText = twilioConfiguration.getAndroid202103VerificationText(); - this.iosVerificationText = twilioConfiguration.getIosVerificationText(); - this.genericVerificationText = twilioConfiguration.getGenericVerificationText(); this.smsUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Messages.json"); this.voxUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Calls.json" ); this.httpClient = FaultTolerantHttpClient.newBuilder() @@ -105,6 +99,9 @@ public class TwilioSmsSender { .withName("twilio") .build(); + this.defaultClientVerificationTexts = twilioConfiguration.getDefaultClientVerificationTexts(); + this.regionalClientVerificationTexts = twilioConfiguration.getRegionalClientVerificationTexts(); + this.dynamicConfigurationManager = dynamicConfigurationManager; this.twilioVerifySender = new TwilioVerifySender(baseVerifyUri, httpClient, twilioConfiguration); } @@ -134,19 +131,25 @@ public class TwilioSmsSender { } private String getBodyFormatString(@Nonnull String destination, @Nullable String clientType) { + + final String countryCode = Util.getCountryCode(destination); + + final TwilioVerificationTextConfiguration verificationTexts = regionalClientVerificationTexts + .getOrDefault(countryCode, defaultClientVerificationTexts); + final String result; if ("ios".equals(clientType)) { - result = iosVerificationText; + result = verificationTexts.getIosText(); } else if ("android-ng".equals(clientType)) { - result = androidNgVerificationText; + result = verificationTexts.getAndroidNgText(); } else if ("android-2020-01".equals(clientType)) { - result = android202001VerificationText; + result = verificationTexts.getAndroid202001Text(); } else if ("android-2021-03".equals(clientType)) { - result = android202103VerificationText; + result = verificationTexts.getAndroid202103Text(); } else { - result = genericVerificationText; + result = verificationTexts.getGenericText(); } - if (destination.startsWith("+86")) { // is China + if ("86".equals(countryCode)) { // is China return result + "\u2008"; // Twilio recommends adding this character to the end of strings delivered to China because some carriers in // China are blocking GSM-7 encoding and this will force Twilio to send using UCS-2 instead. 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 de2b7843a..d7d4185a6 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 @@ -5,20 +5,29 @@ package org.whispersystems.textsecuregcm.tests.sms; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.github.tomakehurst.wiremock.junit.WireMockRule; import java.util.List; import java.util.Locale.LanguageRange; +import java.util.Map; import java.util.Optional; import javax.annotation.Nonnull; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; +import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTwilioConfiguration; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; @@ -62,15 +71,29 @@ public class TwilioSmsSenderTest { configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID); configuration.setVerifyServiceSid(VERIFY_SERVICE_SID); configuration.setLocalDomain(LOCAL_DOMAIN); - configuration.setIosVerificationText("Verify on iOS: %1$s\n\nsomelink://verify/%1$s"); - configuration.setAndroidNgVerificationText("<#> Verify on AndroidNg: %1$s\n\ncharacters"); - configuration.setAndroid202001VerificationText("Verify on Android202001: %1$s\n\nsomelink://verify/%1$s\n\ncharacters"); - configuration.setAndroid202103VerificationText("Verify on Android202103: %1$s\n\ncharacters"); - configuration.setGenericVerificationText("Verify on whatever: %1$s"); + + configuration.setDefaultClientVerificationTexts(createTwlilioVerificationText("")); + + configuration.setRegionalClientVerificationTexts( + Map.of("33", createTwlilioVerificationText("[33] ")) + ); configuration.setAndroidAppHash("someHash"); return configuration; } + private TwilioVerificationTextConfiguration createTwlilioVerificationText(final String prefix) { + + TwilioVerificationTextConfiguration verificationTextConfiguration = new TwilioVerificationTextConfiguration(); + + verificationTextConfiguration.setIosText(prefix + "Verify on iOS: %1$s\n\nsomelink://verify/%1$s"); + verificationTextConfiguration.setAndroidNgText(prefix + "<#> Verify on AndroidNg: %1$s\n\ncharacters"); + verificationTextConfiguration.setAndroid202001Text(prefix + "Verify on Android202001: %1$s\n\nsomelink://verify/%1$s\n\ncharacters"); + verificationTextConfiguration.setAndroid202103Text(prefix + "Verify on Android202103: %1$s\n\ncharacters"); + verificationTextConfiguration.setGenericText(prefix + "Verify on whatever: %1$s"); + + return verificationTextConfiguration; + } + private void setupSuccessStubForSms() { wireMockRule.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) @@ -250,4 +273,18 @@ public class TwilioSmsSenderTest { .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) .withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B861065529988&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters%E2%80%88"))); } + + @Test + public void testSendSmsRegionalVerificationText() { + setupSuccessStubForSms(); + + boolean success = sender.deliverSmsVerification("+33655512673", Optional.of("android-ng"), "123-456").join(); + + assertThat(success).isTrue(); + + verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) + .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) + .withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B33655512673&Body=%5B33%5D+%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters"))); + } + }