From e021286eeef81f0c1920cdbbbc103cdc50a36378 Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Tue, 7 Jul 2020 09:32:11 -0500 Subject: [PATCH] Add configuration by country for sending from alpha IDs --- .../TwilioAlphaIdConfiguration.java | 31 ++++++++ .../configuration/TwilioConfiguration.java | 15 +++- .../textsecuregcm/sms/TwilioSmsSender.java | 57 ++++++++++----- .../tests/sms/TwilioSmsSenderTest.java | 72 +++++++++++-------- 4 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioAlphaIdConfiguration.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioAlphaIdConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioAlphaIdConfiguration.java new file mode 100644 index 000000000..f3ecc3093 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioAlphaIdConfiguration.java @@ -0,0 +1,31 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.google.common.annotations.VisibleForTesting; + +import javax.validation.constraints.NotEmpty; + +public class TwilioAlphaIdConfiguration { + @NotEmpty + private String prefix; + + @NotEmpty + private String value; + + public String getPrefix() { + return prefix; + } + + @VisibleForTesting + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + public String getValue() { + return value; + } + + @VisibleForTesting + public void setValue(String value) { + this.value = value; + } +} 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 197ff271e..1964cb339 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java @@ -18,9 +18,9 @@ package org.whispersystems.textsecuregcm.configuration; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; -import org.hibernate.validator.constraints.NotEmpty; import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.List; @@ -53,6 +53,10 @@ public class TwilioConfiguration { @Valid private RetryConfiguration retry = new RetryConfiguration(); + @NotNull + @Valid + private List alphaId; + public String getAccountId() { return accountId; } @@ -115,4 +119,13 @@ public class TwilioConfiguration { public void setRetry(RetryConfiguration retry) { this.retry = retry; } + + public List getAlphaId() { + return alphaId; + } + + @VisibleForTesting + public void setAlphaId(List alphaId) { + this.alphaId = alphaId; + } } 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 f7e4f4421..193a7b3a3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.TwilioAlphaIdConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher; @@ -41,6 +42,7 @@ import java.net.http.HttpResponse; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; @@ -60,12 +62,13 @@ public class TwilioSmsSender { private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered")); private final Meter priceMeter = metricRegistry.meter(name(getClass(), "price")); - private final String accountId; - private final String accountToken; - private final ArrayList numbers; - private final String messagingServicesId; - private final String localDomain; - private final Random random; + private final String accountId; + private final String accountToken; + private final ArrayList numbers; + private final String messagingServicesId; + private final String localDomain; + private final List alphaId; + private final Random random; private final FaultTolerantHttpClient httpClient; private final URI smsUri; @@ -80,6 +83,7 @@ public class TwilioSmsSender { this.numbers = new ArrayList<>(twilioConfiguration.getNumbers()); this.localDomain = twilioConfiguration.getLocalDomain(); this.messagingServicesId = twilioConfiguration.getMessagingServicesId(); + this.alphaId = twilioConfiguration.getAlphaId(); this.random = new Random(System.currentTimeMillis()); this.smsUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Messages.json"); this.voxUri = URI.create(baseUri + "/2010-04-01/Accounts/" + accountId + "/Calls.json" ); @@ -101,13 +105,7 @@ public class TwilioSmsSender { public CompletableFuture deliverArbitrarySms(String destination, String message) { Map requestParameters = new HashMap<>(); requestParameters.put("To", destination); - - if (Util.isEmpty(messagingServicesId)) { - requestParameters.put("From", getRandom(random, numbers)); - } else { - requestParameters.put("MessagingServiceSid", messagingServicesId); - } - + setOriginationRequestParameter(destination, requestParameters); requestParameters.put("Body", message); HttpRequest request = HttpRequest.newBuilder() @@ -127,12 +125,7 @@ public class TwilioSmsSender { public CompletableFuture deliverSmsVerification(String destination, Optional clientType, String verificationCode) { Map requestParameters = new HashMap<>(); requestParameters.put("To", destination); - - if (Util.isEmpty(messagingServicesId)) { - requestParameters.put("From", getRandom(random, numbers)); - } else { - requestParameters.put("MessagingServiceSid", messagingServicesId); - } + setOriginationRequestParameter(destination, requestParameters); if ("ios".equals(clientType.orElse(null))) { requestParameters.put("Body", String.format(SmsSender.SMS_IOS_VERIFICATION_TEXT, verificationCode, verificationCode)); @@ -182,6 +175,17 @@ public class TwilioSmsSender { .handle(this::processResponse); } + private void setOriginationRequestParameter(String destination, Map requestParameters) { + final Optional alphaIdConfiguration = getAlphaIdConfigurationForNumber(destination); + if (alphaIdConfiguration.isPresent()) { + requestParameters.put("From", alphaIdConfiguration.get().getValue()); + } else if (Util.isEmpty(messagingServicesId)) { + requestParameters.put("From", getRandom(random, numbers)); + } else { + requestParameters.put("MessagingServiceSid", messagingServicesId); + } + } + private String getRandom(Random random, ArrayList elements) { return elements.get(random.nextInt(elements.size())); } @@ -220,6 +224,21 @@ public class TwilioSmsSender { } } + private Optional getAlphaIdConfigurationForNumber(String number) { + if (alphaId == null) { + return Optional.empty(); + } + + final String countryCode = Util.getCountryCode(number); + for (TwilioAlphaIdConfiguration twilioAlphaIdConfiguration : alphaId) { + if (twilioAlphaIdConfiguration.getPrefix().equalsIgnoreCase(countryCode)) { + return Optional.of(twilioAlphaIdConfiguration); + } + } + + return Optional.empty(); + } + public static class TwilioResponse { private TwilioSuccessResponse successResponse; 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 124023b02..d76a3ebb5 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 @@ -1,11 +1,13 @@ -package org.whispersystems.sms; +package org.whispersystems.textsecuregcm.tests.sms; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Rule; import org.junit.Test; +import org.whispersystems.textsecuregcm.configuration.TwilioAlphaIdConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; @@ -24,6 +26,17 @@ public class TwilioSmsSenderTest { @Rule public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort().dynamicHttpsPort()); + @Nonnull + private TwilioConfiguration createTwilioConfiguration() { + TwilioConfiguration configuration = new TwilioConfiguration(); + configuration.setAccountId(ACCOUNT_ID); + configuration.setAccountToken(ACCOUNT_TOKEN); + configuration.setNumbers(NUMBERS); + configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); + configuration.setLocalDomain(LOCAL_DOMAIN); + return configuration; + } + @Test public void testSendSms() { wireMockRule.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) @@ -33,12 +46,7 @@ public class TwilioSmsSenderTest { .withBody("{\"price\": -0.00750, \"status\": \"sent\"}"))); - TwilioConfiguration configuration = new TwilioConfiguration(); - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setNumbers(NUMBERS); - configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); - configuration.setLocalDomain(LOCAL_DOMAIN); + TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); @@ -59,12 +67,7 @@ public class TwilioSmsSenderTest { .withBody("{\"price\": -0.00750, \"status\": \"completed\"}"))); - TwilioConfiguration configuration = new TwilioConfiguration(); - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setNumbers(NUMBERS); - configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); - configuration.setLocalDomain(LOCAL_DOMAIN); + TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); boolean success = sender.deliverVoxVerification("+14153333333", "123-456", Optional.of("en_US")).join(); @@ -86,12 +89,7 @@ public class TwilioSmsSenderTest { .withBody("{\"message\": \"Server error!\"}"))); - TwilioConfiguration configuration = new TwilioConfiguration(); - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setNumbers(NUMBERS); - configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); - configuration.setLocalDomain(LOCAL_DOMAIN); + TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); @@ -112,12 +110,7 @@ public class TwilioSmsSenderTest { .withHeader("Content-Type", "application/json") .withBody("{\"message\": \"Server error!\"}"))); - TwilioConfiguration configuration = new TwilioConfiguration(); - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setNumbers(NUMBERS); - configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); - configuration.setLocalDomain(LOCAL_DOMAIN); + TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); boolean success = sender.deliverVoxVerification("+14153333333", "123-456", Optional.of("en_US")).join(); @@ -132,12 +125,7 @@ public class TwilioSmsSenderTest { @Test public void testSendSmsNetworkFailure() { - TwilioConfiguration configuration = new TwilioConfiguration(); - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setNumbers(NUMBERS); - configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); - configuration.setLocalDomain(LOCAL_DOMAIN); + TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + 39873, configuration); boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); @@ -145,6 +133,28 @@ public class TwilioSmsSenderTest { assertThat(success).isFalse(); } + @Test + public void testSendAlphaIdByCountryCode() { + wireMockRule.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) + .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"price\": -0.00750, \"status\": \"sent\"}"))); + TwilioConfiguration configuration = createTwilioConfiguration(); + TwilioAlphaIdConfiguration alphaIdConfiguration = new TwilioAlphaIdConfiguration(); + alphaIdConfiguration.setPrefix("852"); + alphaIdConfiguration.setValue("SIGNAL"); + configuration.setAlphaId(List.of(alphaIdConfiguration)); + + TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); + boolean success = sender.deliverSmsVerification("+85278675309", Optional.of("android-ng"), "987-654").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("To=%2B85278675309&From=SIGNAL&Body=%3C%23%3E+Your+Signal+verification+code%3A+987-654%0A%0AdoDiFGKPO1r"))); + } }