diff --git a/service/config/sample.yml b/service/config/sample.yml index 00b6e95bd..ada3b37af 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -7,8 +7,29 @@ twilio: # Twilio gateway configuration - # Third number - # ... - # Nth number - messagingServicesId: + nanpaMessagingServiceSid: # Twilio SID for the messaging service to use for NANPA. + messagingServiceSid: # Twilio SID for the message service to use for non-NANPA. 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. + genericVerificationText: # Text to use when the client type is unrecognized. Will be passed to String.format with the verification code as argument 1. + senderId: + defaultSenderId: # Sender ID to use for country codes not found in either the overrides or omitted lists. + countryCodesWithoutSenderId: + - # First country code + - # Second country code + - # ... + - # Nth country code + countrySpecificSenderIds: + - countryCode: # First country code + senderId: # Sender ID to use for this country + - countryCode: # Second country code + senderId: # Sender ID to use for this country + - countryCode: # ... + senderId: # ... + - countryCode: # Nth country code + senderId: # Sender ID to use for this country push: queueSize: # Size of push pending queue 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 2c34f7417..0b5333016 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java @@ -1,18 +1,18 @@ -/** - * Copyright (C) 2013 Open WhisperSystems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . +/* + Copyright (C) 2013 Open WhisperSystems + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ package org.whispersystems.textsecuregcm.configuration; @@ -37,7 +37,8 @@ public class TwilioConfiguration { @NotEmpty private String localDomain; - private String messagingServicesId; + private String messagingServiceSid; + private String nanpaMessagingServiceSid; @NotNull @Valid @@ -99,13 +100,22 @@ public class TwilioConfiguration { this.localDomain = localDomain; } - public String getMessagingServicesId() { - return messagingServicesId; + public String getMessagingServiceSid() { + return messagingServiceSid; } @VisibleForTesting - public void setMessagingServicesId(String messagingServicesId) { - this.messagingServicesId = messagingServicesId; + public void setMessagingServiceSid(String messagingServiceSid) { + this.messagingServiceSid = messagingServiceSid; + } + + public String getNanpaMessagingServiceSid() { + return nanpaMessagingServiceSid; + } + + @VisibleForTesting + public void setNanpaMessagingServiceSid(String nanpaMessagingServiceSid) { + this.nanpaMessagingServiceSid = nanpaMessagingServiceSid; } public CircuitBreakerConfiguration getCircuitBreaker() { 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 e557a8761..3bac041ca 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -21,6 +21,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.annotation.Timed; import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.Auth; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; @@ -84,7 +85,6 @@ import java.util.Optional; import java.util.UUID; import static com.codahale.metrics.MetricRegistry.name; -import io.dropwizard.auth.Auth; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Path("/v1/accounts") @@ -227,11 +227,6 @@ public class AccountController { throw new WebApplicationException(Response.status(422).build()); } - if (userAgent != null && userAgent.stream().anyMatch(header -> header.toLowerCase().contains("okhttp"))) { - smsSender.deliverUpdatetoSignalSms(number); - return Response.ok().build(); - } - VerificationCode verificationCode = generateVerificationCode(number); StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), System.currentTimeMillis(), 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 4c0a119d9..af9d9b8ea 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java @@ -27,10 +27,6 @@ public class SmsSender { this.twilioSender = twilioSender; } - public void deliverUpdatetoSignalSms(String destination) { - twilioSender.deliverArbitrarySms(destination, "To continue installing, update to Signal: https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms"); - } - public void deliverSmsVerification(String destination, Optional clientType, String verificationCode) { // Fix up mexico numbers to 'mobile' format just for SMS delivery. if (destination.startsWith("+52") && !destination.startsWith("+521")) { 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 f4a86367c..f2c932069 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java @@ -22,6 +22,7 @@ import com.codahale.metrics.SharedMetricRegistries; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; @@ -57,7 +58,6 @@ public class TwilioSmsSender { private static final Logger logger = LoggerFactory.getLogger(TwilioSmsSender.class); private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); - private final Meter arbitraryMeter = metricRegistry.meter(name(getClass(), "arbitrary", "delivered")); private final Meter smsMeter = metricRegistry.meter(name(getClass(), "sms", "delivered")); private final Meter voxMeter = metricRegistry.meter(name(getClass(), "vox", "delivered")); private final Meter priceMeter = metricRegistry.meter(name(getClass(), "price")); @@ -65,7 +65,8 @@ public class TwilioSmsSender { private final String accountId; private final String accountToken; private final ArrayList numbers; - private final String messagingServicesId; + private final String messagingServiceSid; + private final String nanpaMessagingServiceSid; private final String localDomain; private final SenderIdSupplier senderIdSupplier; private final Random random; @@ -86,7 +87,8 @@ public class TwilioSmsSender { this.accountToken = twilioConfiguration.getAccountToken(); this.numbers = new ArrayList<>(twilioConfiguration.getNumbers()); this.localDomain = twilioConfiguration.getLocalDomain(); - this.messagingServicesId = twilioConfiguration.getMessagingServicesId(); + this.messagingServiceSid = twilioConfiguration.getMessagingServiceSid(); + this.nanpaMessagingServiceSid = twilioConfiguration.getNanpaMessagingServiceSid(); this.senderIdSupplier = new SenderIdSupplier(twilioConfiguration.getSenderId()); this.random = new Random(System.currentTimeMillis()); this.androidNgVerificationText = twilioConfiguration.getAndroidNgVerificationText(); @@ -110,26 +112,6 @@ public class TwilioSmsSender { this("https://api.twilio.com", twilioConfiguration); } - public CompletableFuture deliverArbitrarySms(String destination, String message) { - Map requestParameters = new HashMap<>(); - requestParameters.put("To", destination); - setOriginationRequestParameter(destination, requestParameters); - requestParameters.put("Body", message); - - HttpRequest request = HttpRequest.newBuilder() - .uri(smsUri) - .POST(FormDataBodyPublisher.of(requestParameters)) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Authorization", "Basic " + Base64.encodeBytes((accountId + ":" + accountToken).getBytes())) - .build(); - - arbitraryMeter.mark(); - - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(this::parseResponse) - .handle(this::processResponse); - } - public CompletableFuture deliverSmsVerification(String destination, Optional clientType, String verificationCode) { Map requestParameters = new HashMap<>(); requestParameters.put("To", destination); @@ -193,10 +175,12 @@ public class TwilioSmsSender { final Optional senderId = senderIdSupplier.get(destination); if (senderId.isPresent()) { requestParameters.put("From", senderId.get()); - } else if (Util.isEmpty(messagingServicesId)) { - requestParameters.put("From", getRandom(random, numbers)); + } else if (StringUtils.isNotEmpty(nanpaMessagingServiceSid) && "1".equals(Util.getCountryCode(destination))) { + requestParameters.put("MessagingServiceSid", nanpaMessagingServiceSid); + } else if (StringUtils.isNotEmpty(messagingServiceSid)) { + requestParameters.put("MessagingServiceSid", messagingServiceSid); } else { - requestParameters.put("MessagingServiceSid", messagingServicesId); + requestParameters.put("From", getRandom(random, numbers)); } } 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 42366b6ca..8b7471c78 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 @@ -28,11 +28,12 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; public class TwilioSmsSenderTest { - private static final String ACCOUNT_ID = "test_account_id"; - private static final String ACCOUNT_TOKEN = "test_account_token"; - private static final List NUMBERS = List.of("+14151111111", "+14152222222"); - private static final String MESSAGING_SERVICES_ID = "test_messaging_services_id"; - private static final String LOCAL_DOMAIN = "test.com"; + private static final String ACCOUNT_ID = "test_account_id"; + private static final String ACCOUNT_TOKEN = "test_account_token"; + private static final List NUMBERS = List.of("+14151111111", "+14152222222"); + private static final String MESSAGING_SERVICE_SID = "test_messaging_services_id"; + private static final String NANPA_MESSAGING_SERVICE_SID = "nanpa_test_messaging_service_id"; + private static final String LOCAL_DOMAIN = "test.com"; @Rule public WireMockRule wireMockRule = new WireMockRule(options().dynamicPort().dynamicHttpsPort()); @@ -43,7 +44,7 @@ public class TwilioSmsSenderTest { configuration.setAccountId(ACCOUNT_ID); configuration.setAccountToken(ACCOUNT_TOKEN); configuration.setNumbers(NUMBERS); - configuration.setMessagingServicesId(MESSAGING_SERVICES_ID); + configuration.setMessagingServiceSid(MESSAGING_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"); @@ -52,15 +53,17 @@ public class TwilioSmsSenderTest { return configuration; } + private void setupSuccessStubForSms() { + 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\"}"))); + } + @Test public void testSendSms() { - 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\"}"))); - - + setupSuccessStubForSms(); TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); @@ -74,12 +77,7 @@ public class TwilioSmsSenderTest { @Test public void testSendSmsAndroid202001() { - 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\"}"))); - + setupSuccessStubForSms(); TwilioConfiguration configuration = createTwilioConfiguration(); TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-2020-01"), "123-456").join(); @@ -91,6 +89,25 @@ public class TwilioSmsSenderTest { .withRequestBody(equalTo("MessagingServiceSid=test_messaging_services_id&To=%2B14153333333&Body=Verify+on+Android202001%3A+123-456%0A%0Asomelink%3A%2F%2Fverify%2F123-456%0A%0Acharacters"))); } + @Test + public void testSendSmsNanpaMessagingService() { + setupSuccessStubForSms(); + TwilioConfiguration configuration = createTwilioConfiguration(); + configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID); + TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMockRule.port(), configuration); + + assertThat(sender.deliverSmsVerification("+14153333333", Optional.of("ios"), "654-321").join()).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=nanpa_test_messaging_service_id&To=%2B14153333333&Body=Verify+on+iOS%3A+654-321%0A%0Asomelink%3A%2F%2Fverify%2F654-321"))); + + wireMockRule.resetRequests(); + assertThat(sender.deliverSmsVerification("+447911123456", Optional.of("ios"), "654-321").join()).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=%2B447911123456&Body=Verify+on+iOS%3A+654-321%0A%0Asomelink%3A%2F%2Fverify%2F654-321"))); + } + @Test public void testSendVox() { wireMockRule.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json")) @@ -169,11 +186,7 @@ public class TwilioSmsSenderTest { private void runSenderIdTest(String destination, String expectedSenderId, Supplier senderIdConfigurationSupplier) { - 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\"}"))); + setupSuccessStubForSms(); TwilioConfiguration configuration = createTwilioConfiguration(); configuration.setSenderId(senderIdConfigurationSupplier.get()); @@ -189,7 +202,7 @@ public class TwilioSmsSenderTest { if (expectedSenderId != null) { expectedRequestBody = requestBodyToParam + "&From=" + expectedSenderId + requestBodySuffix; } else { - expectedRequestBody = "MessagingServiceSid=" + MESSAGING_SERVICES_ID + "&" + requestBodyToParam + requestBodySuffix; + expectedRequestBody = "MessagingServiceSid=" + MESSAGING_SERVICE_SID + "&" + requestBodyToParam + requestBodySuffix; } verify(1, postRequestedFor(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))