From 74d65b37a804617e8489ee946152cc84077a0b96 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Fri, 7 Oct 2022 17:11:37 -0400 Subject: [PATCH] Discard old Twilio machinery and rely entirely on the stand-alone registration service --- service/config/sample.yml | 23 - .../WhisperServerConfiguration.java | 10 - .../textsecuregcm/WhisperServerService.java | 14 +- .../configuration/TwilioConfiguration.java | 159 ----- .../TwilioCountrySenderIdConfiguration.java | 36 -- .../TwilioVerificationTextConfiguration.java | 67 -- .../dynamic/DynamicConfiguration.java | 13 - .../dynamic/DynamicTwilioConfiguration.java | 23 - .../controllers/AccountController.java | 185 +----- .../textsecuregcm/sms/SmsSender.java | 57 -- .../textsecuregcm/sms/TwilioSmsSender.java | 345 ---------- ...ilioVerifyExperimentEnrollmentManager.java | 74 --- .../textsecuregcm/sms/TwilioVerifySender.java | 324 ---------- .../dynamic/DynamicConfigurationTest.java | 26 - .../controllers/AccountControllerTest.java | 599 +++++------------- ...VerifyExperimentEnrollmentManagerTest.java | 89 --- .../sms/TwilioVerifySenderTest.java | 248 -------- .../tests/sms/SmsSenderTest.java | 42 -- .../tests/sms/TwilioSmsSenderTest.java | 291 --------- 19 files changed, 159 insertions(+), 2466 deletions(-) delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioCountrySenderIdConfiguration.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTwilioConfiguration.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManager.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySender.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManagerTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySenderTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/SmsSenderTest.java delete mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/TwilioSmsSenderTest.java diff --git a/service/config/sample.yml b/service/config/sample.yml index 7e3ae6dec..ab694809a 100644 --- a/service/config/sample.yml +++ b/service/config/sample.yml @@ -62,29 +62,6 @@ dynamoDbTables: subscriptions: tableName: Example_Subscriptions -twilio: # Twilio gateway configuration - accountId: unset - accountToken: unset - nanpaMessagingServiceSid: unset # Twilio SID for the messaging service to use for NANPA. - messagingServiceSid: unset # Twilio SID for the message service to use for non-NANPA. - verifyServiceSid: unset # Twilio SID for a Verify service - localDomain: example.com # Domain Twilio can connect back to for calls. Should be domain of your service. - defaultClientVerificationTexts: - ios: example %1$s # Text to use for the verification message on iOS. Will be passed to String.format with the verification code as argument 1. - androidNg: example %1$s # 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: example %1$s # 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: example %1$s # 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: example %1$s # 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: example %1$s # all keys from defaultClientVerificationTexts are required - androidNg: example %1$s - android202001: example %1$s - android202103: example %1$s - generic: example %1$s - androidAppHash: example # Hash appended to Android - verifyServiceFriendlyName: example # Service name used in template. Requires Twilio account rep to enable - cacheCluster: # Redis server configuration for cache cluster configurationUri: redis://redis.example.com:6379/ diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 0a7bc4a32..be8f0ad4a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -45,7 +45,6 @@ import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfig import org.whispersystems.textsecuregcm.configuration.StripeConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration; -import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration; import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration; import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration; @@ -75,11 +74,6 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private DynamoDbTables dynamoDbTables; - @NotNull - @Valid - @JsonProperty - private TwilioConfiguration twilio; - @NotNull @Valid @JsonProperty @@ -297,10 +291,6 @@ public class WhisperServerConfiguration extends Configuration { return webSocket; } - public TwilioConfiguration getTwilioConfiguration() { - return twilio; - } - public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() { return awsAttachments; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index c12ed591e..5967a0ea0 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -163,9 +163,6 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner; import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; -import org.whispersystems.textsecuregcm.sms.SmsSender; -import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; -import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.AccountCleaner; @@ -440,9 +437,6 @@ public class WhisperServerService extends Application commonControllers = Lists.newArrayList( diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java deleted file mode 100644 index cbc614bc5..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioConfiguration.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ -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; - -public class TwilioConfiguration { - - @NotEmpty - private String accountId; - - @NotEmpty - private String accountToken; - - @NotEmpty - private String localDomain; - - @NotEmpty - private String messagingServiceSid; - - @NotEmpty - private String nanpaMessagingServiceSid; - - @NotEmpty - private String verifyServiceSid; - - @NotNull - @Valid - private CircuitBreakerConfiguration circuitBreaker = new CircuitBreakerConfiguration(); - - @NotNull - @Valid - private RetryConfiguration retry = new RetryConfiguration(); - - @Valid - private TwilioVerificationTextConfiguration defaultClientVerificationTexts; - - @Valid - private Map regionalClientVerificationTexts = Collections.emptyMap(); - - @NotEmpty - private String androidAppHash; - - @NotEmpty - private String verifyServiceFriendlyName; - - public String getAccountId() { - return accountId; - } - - @VisibleForTesting - public void setAccountId(String accountId) { - this.accountId = accountId; - } - - public String getAccountToken() { - return accountToken; - } - - @VisibleForTesting - public void setAccountToken(String accountToken) { - this.accountToken = accountToken; - } - public String getLocalDomain() { - return localDomain; - } - - @VisibleForTesting - public void setLocalDomain(String localDomain) { - this.localDomain = localDomain; - } - - public String getMessagingServiceSid() { - return messagingServiceSid; - } - - @VisibleForTesting - public void setMessagingServiceSid(String messagingServiceSid) { - this.messagingServiceSid = messagingServiceSid; - } - - public String getNanpaMessagingServiceSid() { - return nanpaMessagingServiceSid; - } - - @VisibleForTesting - public void setNanpaMessagingServiceSid(String nanpaMessagingServiceSid) { - this.nanpaMessagingServiceSid = nanpaMessagingServiceSid; - } - - public String getVerifyServiceSid() { - return verifyServiceSid; - } - - @VisibleForTesting - public void setVerifyServiceSid(String verifyServiceSid) { - this.verifyServiceSid = verifyServiceSid; - } - - public CircuitBreakerConfiguration getCircuitBreaker() { - return circuitBreaker; - } - - @VisibleForTesting - public void setCircuitBreaker(CircuitBreakerConfiguration circuitBreaker) { - this.circuitBreaker = circuitBreaker; - } - - public RetryConfiguration getRetry() { - return retry; - } - - @VisibleForTesting - public void setRetry(RetryConfiguration retry) { - this.retry = retry; - } - - public TwilioVerificationTextConfiguration getDefaultClientVerificationTexts() { - return defaultClientVerificationTexts; - } - - @VisibleForTesting - public void setDefaultClientVerificationTexts(TwilioVerificationTextConfiguration defaultClientVerificationTexts) { - this.defaultClientVerificationTexts = defaultClientVerificationTexts; - } - - - public Map getRegionalClientVerificationTexts() { - return regionalClientVerificationTexts; - } - - @VisibleForTesting - public void setRegionalClientVerificationTexts(final Map regionalClientVerificationTexts) { - this.regionalClientVerificationTexts = regionalClientVerificationTexts; - } - - public String getAndroidAppHash() { - return androidAppHash; - } - - public void setAndroidAppHash(String androidAppHash) { - this.androidAppHash = androidAppHash; - } - - public void setVerifyServiceFriendlyName(String serviceFriendlyName) { - this.verifyServiceFriendlyName = serviceFriendlyName; - } - - public String getVerifyServiceFriendlyName() { - return verifyServiceFriendlyName; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioCountrySenderIdConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioCountrySenderIdConfiguration.java deleted file mode 100644 index b330c48e5..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioCountrySenderIdConfiguration.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.configuration; - -import com.google.common.annotations.VisibleForTesting; - -import javax.validation.constraints.NotEmpty; - -public class TwilioCountrySenderIdConfiguration { - @NotEmpty - private String countryCode; - - @NotEmpty - private String senderId; - - public String getCountryCode() { - return countryCode; - } - - @VisibleForTesting - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } - - public String getSenderId() { - return senderId; - } - - @VisibleForTesting - public void setSenderId(String senderId) { - this.senderId = senderId; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java deleted file mode 100644 index 1affb77a2..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/TwilioVerificationTextConfiguration.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.whispersystems.textsecuregcm.configuration; - -import com.fasterxml.jackson.annotation.JsonProperty; -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/configuration/dynamic/DynamicConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfiguration.java index 0c6a1589b..fbbfe9706 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfiguration.java @@ -29,10 +29,6 @@ public class DynamicConfiguration { @Valid private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration(); - @JsonProperty - @Valid - private DynamicTwilioConfiguration twilio = new DynamicTwilioConfiguration(); - @JsonProperty @Valid private DynamicCaptchaConfiguration captcha = new DynamicCaptchaConfiguration(); @@ -86,15 +82,6 @@ public class DynamicConfiguration { return payments; } - public DynamicTwilioConfiguration getTwilioConfiguration() { - return twilio; - } - - @VisibleForTesting - public void setTwilioConfiguration(DynamicTwilioConfiguration twilioConfiguration) { - this.twilio = twilioConfiguration; - } - public DynamicCaptchaConfiguration getCaptchaConfiguration() { return captcha; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTwilioConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTwilioConfiguration.java deleted file mode 100644 index b4d4ea767..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicTwilioConfiguration.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.whispersystems.textsecuregcm.configuration.dynamic; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.annotations.VisibleForTesting; -import javax.validation.constraints.NotNull; -import java.util.Collections; -import java.util.List; - -public class DynamicTwilioConfiguration { - - @JsonProperty - @NotNull - private List numbers = Collections.emptyList(); - - public List getNumbers() { - return numbers; - } - - @VisibleForTesting - public void setNumbers(List numbers) { - this.numbers = numbers; - } -} 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 05bd127da..1bb9eace8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -21,13 +21,9 @@ import io.micrometer.core.instrument.Tags; import java.security.SecureRandom; import java.time.Duration; import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; @@ -83,7 +79,6 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse; import org.whispersystems.textsecuregcm.entities.StaleDevices; import org.whispersystems.textsecuregcm.entities.UsernameRequest; import org.whispersystems.textsecuregcm.entities.UsernameResponse; -import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; @@ -93,8 +88,6 @@ import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.registration.ClientType; import org.whispersystems.textsecuregcm.registration.MessageTransport; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; -import org.whispersystems.textsecuregcm.sms.SmsSender; -import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; @@ -112,7 +105,6 @@ import org.whispersystems.textsecuregcm.util.NonNormalizedPhoneNumberException; import org.whispersystems.textsecuregcm.util.Optionals; import org.whispersystems.textsecuregcm.util.UsernameGenerator; import org.whispersystems.textsecuregcm.util.Util; -import org.whispersystems.textsecuregcm.util.VerificationCode; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Path("/v1/accounts") @@ -133,10 +125,6 @@ public class AccountController { private static final String CAPTCHA_ATTEMPT_COUNTER_NAME = name(AccountController.class, "captcha"); private static final String CHALLENGE_ISSUED_COUNTER_NAME = name(AccountController.class, "challengeIssued"); - private static final String TWILIO_VERIFY_ERROR_COUNTER_NAME = name(AccountController.class, "twilioVerifyError"); - private static final String TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME = name(AccountController.class, "twilioUndelivered"); - - private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(AccountController.class, "invalidAcceptLanguage"); private static final String NONSTANDARD_USERNAME_COUNTER_NAME = name(AccountController.class, "nonStandardUsername"); private static final String CHALLENGE_PRESENT_TAG_NAME = "present"; @@ -157,7 +145,6 @@ public class AccountController { private final AccountsManager accounts; private final AbusiveHostRules abusiveHostRules; private final RateLimiters rateLimiters; - private final SmsSender smsSender; private final RegistrationServiceClient registrationServiceClient; private final DynamicConfigurationManager dynamicConfigurationManager; private final TurnTokenGenerator turnTokenGenerator; @@ -166,13 +153,8 @@ public class AccountController { private final PushNotificationManager pushNotificationManager; private final ExternalServiceCredentialGenerator backupServiceCredentialGenerator; - private final TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager; - private final ExperimentEnrollmentManager experimentEnrollmentManager; private final ChangeNumberManager changeNumberManager; - @VisibleForTesting - static final String REGISTRATION_SERVICE_EXPERIMENT_NAME = "registration-service"; - @VisibleForTesting static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); @@ -180,33 +162,27 @@ public class AccountController { AccountsManager accounts, AbusiveHostRules abusiveHostRules, RateLimiters rateLimiters, - SmsSender smsSenderFactory, RegistrationServiceClient registrationServiceClient, DynamicConfigurationManager dynamicConfigurationManager, TurnTokenGenerator turnTokenGenerator, Map testDevices, RecaptchaClient recaptchaClient, PushNotificationManager pushNotificationManager, - TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager, ChangeNumberManager changeNumberManager, - ExternalServiceCredentialGenerator backupServiceCredentialGenerator, - final ExperimentEnrollmentManager experimentEnrollmentManager) + ExternalServiceCredentialGenerator backupServiceCredentialGenerator) { this.pendingAccounts = pendingAccounts; this.accounts = accounts; this.abusiveHostRules = abusiveHostRules; this.rateLimiters = rateLimiters; - this.smsSender = smsSenderFactory; this.registrationServiceClient = registrationServiceClient; this.dynamicConfigurationManager = dynamicConfigurationManager; this.testDevices = testDevices; this.turnTokenGenerator = turnTokenGenerator; this.recaptchaClient = recaptchaClient; this.pushNotificationManager = pushNotificationManager; - this.verifyExperimentEnrollmentManager = verifyExperimentEnrollmentManager; this.backupServiceCredentialGenerator = backupServiceCredentialGenerator; this.changeNumberManager = changeNumberManager; - this.experimentEnrollmentManager = experimentEnrollmentManager; } @Timed @@ -304,127 +280,6 @@ public class AccountController { default -> throw new WebApplicationException(Response.status(422).build()); } - if (experimentEnrollmentManager.isEnrolled(number, REGISTRATION_SERVICE_EXPERIMENT_NAME)) { - sendVerificationCodeViaRegistrationService(number, - maybeStoredVerificationCode, - acceptLanguage, - client, - transport); - } else { - sendVerificationCodeViaTwilioSender(number, - maybeStoredVerificationCode, - acceptLanguage, - userAgent, - client, - transport, - assessmentResult); - } - - Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of( - UserAgentTagUtil.getPlatformTag(userAgent), - Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)), - Tag.of(REGION_TAG_NAME, Util.getRegion(number)), - Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport))) - .increment(); - - return Response.ok().build(); - } - - private void sendVerificationCodeViaTwilioSender(final String number, - final Optional maybeStoredVerificationCode, - final Optional acceptLanguage, - final String userAgent, - final Optional client, - final String transport, - final Optional assessmentResult) { - final VerificationCode verificationCode = generateVerificationCode(number); - - final StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), - System.currentTimeMillis(), - maybeStoredVerificationCode.map(StoredVerificationCode::pushCode).orElse(null), - maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid).orElse(null), - maybeStoredVerificationCode.map(StoredVerificationCode::sessionId).orElse(null)); - - pendingAccounts.store(number, storedVerificationCode); - - List languageRanges; - try { - languageRanges = acceptLanguage.map(Locale.LanguageRange::parse).orElse(Collections.emptyList()); - } catch (final IllegalArgumentException e) { - logger.debug("Could not get acceptable languages; Accept-Language: {}; User-Agent: {}", - acceptLanguage.orElse(""), - userAgent, - e); - - Metrics.counter(INVALID_ACCEPT_LANGUAGE_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment(); - languageRanges = Collections.emptyList(); - } - - final boolean enrolledInVerifyExperiment = verifyExperimentEnrollmentManager.isEnrolled(client, number, languageRanges, transport); - final CompletableFuture> sendVerificationWithTwilioVerifyFuture; - - if (testDevices.containsKey(number)) { - // noop - sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty()); - } else if (transport.equals("sms")) { - - if (enrolledInVerifyExperiment) { - sendVerificationWithTwilioVerifyFuture = smsSender.deliverSmsVerificationWithTwilioVerify(number, client, verificationCode.getVerificationCode(), languageRanges); - } else { - smsSender.deliverSmsVerification(number, client, verificationCode.getVerificationCodeDisplay()); - sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty()); - } - } else if (transport.equals("voice")) { - - if (enrolledInVerifyExperiment) { - sendVerificationWithTwilioVerifyFuture = smsSender.deliverVoxVerificationWithTwilioVerify(number, verificationCode.getVerificationCode(), languageRanges); - } else { - smsSender.deliverVoxVerification(number, verificationCode.getVerificationCode(), languageRanges); - sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty()); - } - - } else { - sendVerificationWithTwilioVerifyFuture = CompletableFuture.completedFuture(Optional.empty()); - } - - sendVerificationWithTwilioVerifyFuture.whenComplete((maybeVerificationSid, throwable) -> { - if (throwable != null) { - Metrics.counter(TWILIO_VERIFY_ERROR_COUNTER_NAME).increment(); - - logger.warn("Error with Twilio Verify", throwable); - return; - } - - if (enrolledInVerifyExperiment && maybeVerificationSid.isEmpty() && assessmentResult.isPresent()) { - final String countryCode = Util.getCountryCode(number); - final String region = Util.getRegion(number); - - Metrics.counter(TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME, Tags.of( - Tag.of(COUNTRY_CODE_TAG_NAME, countryCode), - Tag.of(REGION_TAG_NAME, region), - UserAgentTagUtil.getPlatformTag(userAgent), - Tag.of(SCORE_TAG_NAME, assessmentResult.get().score()))) - .increment(); - } - - maybeVerificationSid.ifPresent(twilioVerificationSid -> { - StoredVerificationCode storedVerificationCodeWithVerificationSid = new StoredVerificationCode( - storedVerificationCode.code(), - storedVerificationCode.timestamp(), - storedVerificationCode.pushCode(), - twilioVerificationSid, - storedVerificationCode.sessionId()); - pendingAccounts.store(number, storedVerificationCodeWithVerificationSid); - }); - }); - } - - private void sendVerificationCodeViaRegistrationService(final String number, - final Optional maybeStoredVerificationCode, - final Optional acceptLanguage, - final Optional client, - final String transport) { - final Phonenumber.PhoneNumber phoneNumber; try { @@ -461,6 +316,15 @@ public class AccountController { sessionId); pendingAccounts.store(number, storedVerificationCode); + + Metrics.counter(ACCOUNT_CREATE_COUNTER_NAME, Tags.of( + UserAgentTagUtil.getPlatformTag(userAgent), + Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)), + Tag.of(REGION_TAG_NAME, Util.getRegion(number)), + Tag.of(VERIFICATION_TRANSPORT_TAG_NAME, transport))) + .increment(); + + return Response.ok().build(); } @Timed @@ -497,10 +361,6 @@ public class AccountController { throw new WebApplicationException(Response.status(403).build()); } - maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid) - .ifPresent( - verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "registration")); - Optional existingAccount = accounts.getByE164(number); if (existingAccount.isPresent()) { @@ -552,23 +412,15 @@ public class AccountController { rateLimiters.getVerifyLimiter().validate(number); - final Optional maybeStoredVerificationCode = pendingAccounts.getCodeForNumber(number); - - final boolean codeVerified = maybeStoredVerificationCode.map(storedVerificationCode -> - storedVerificationCode.sessionId() != null ? - registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(), - request.code(), REGISTRATION_RPC_TIMEOUT).join() : - storedVerificationCode.isValid(request.code())) + final boolean codeVerified = pendingAccounts.getCodeForNumber(number).map(storedVerificationCode -> + registrationServiceClient.checkVerificationCode(storedVerificationCode.sessionId(), + request.code(), REGISTRATION_RPC_TIMEOUT).join()) .orElse(false); if (!codeVerified) { throw new ForbiddenException(); } - maybeStoredVerificationCode.map(StoredVerificationCode::twilioVerificationSid) - .ifPresent( - verificationSid -> smsSender.reportVerificationSucceeded(verificationSid, userAgent, "changeNumber")); - final Optional existingAccount = accounts.getByE164(number); if (existingAccount.isPresent()) { @@ -1039,17 +891,6 @@ public class AccountController { return false; } - @VisibleForTesting protected - VerificationCode generateVerificationCode(String number) { - if (testDevices.containsKey(number)) { - return new VerificationCode(testDevices.get(number)); - } - - SecureRandom random = new SecureRandom(); - int randomInt = 100000 + random.nextInt(900000); - return new VerificationCode(randomInt); - } - private String generatePushChallenge() { SecureRandom random = new SecureRandom(); byte[] challenge = new byte[16]; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java deleted file mode 100644 index f49c20636..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/SmsSender.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ -package org.whispersystems.textsecuregcm.sms; - - -import java.util.List; -import java.util.Locale.LanguageRange; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nullable; - -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class SmsSender { - - private final TwilioSmsSender twilioSender; - - public SmsSender(TwilioSmsSender twilioSender) { - this.twilioSender = twilioSender; - } - - 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")) { - destination = "+521" + destination.substring("+52".length()); - } - - twilioSender.deliverSmsVerification(destination, clientType, verificationCode); - } - - public void deliverVoxVerification(String destination, String verificationCode, List languageRanges) { - twilioSender.deliverVoxVerification(destination, verificationCode, languageRanges); - } - - public CompletableFuture> deliverSmsVerificationWithTwilioVerify(String destination, - Optional clientType, - String verificationCode, List languageRanges) { - // Fix up mexico numbers to 'mobile' format just for SMS delivery. - if (destination.startsWith("+52") && !destination.startsWith("+521")) { - destination = "+521" + destination.substring(3); - } - - return twilioSender.deliverSmsVerificationWithVerify(destination, clientType, verificationCode, languageRanges); - } - - public CompletableFuture> deliverVoxVerificationWithTwilioVerify(String destination, - String verificationCode, - List languageRanges) { - - return twilioSender.deliverVoxVerificationWithVerify(destination, verificationCode, languageRanges); - } - - public void reportVerificationSucceeded(String verificationSid, @Nullable String userAgent, String context) { - twilioSender.reportVerificationSucceeded(verificationSid, userAgent, context); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java deleted file mode 100644 index 3f416434e..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioSmsSender.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ -package org.whispersystems.textsecuregcm.sms; - -import static com.codahale.metrics.MetricRegistry.name; - -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.SharedMetricRegistries; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.annotations.VisibleForTesting; -import io.micrometer.core.instrument.Metrics; -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Base64; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Locale.LanguageRange; -import java.util.Map; -import java.util.Optional; -import java.util.Random; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.stream.Collectors; -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; -import org.whispersystems.textsecuregcm.configuration.TwilioVerificationTextConfiguration; -import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; -import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; -import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; -import org.whispersystems.textsecuregcm.util.Constants; -import org.whispersystems.textsecuregcm.util.ExecutorUtils; -import org.whispersystems.textsecuregcm.util.SystemMapper; -import org.whispersystems.textsecuregcm.util.Util; - -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -public class TwilioSmsSender { - private static final Logger logger = LoggerFactory.getLogger(TwilioSmsSender.class); - - private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); - 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")); - - static final String FAILED_REQUEST_COUNTER_NAME = name(TwilioSmsSender.class, "failedRequest"); - static final String SERVICE_NAME_TAG = "service"; - static final String STATUS_CODE_TAG_NAME = "statusCode"; - static final String ERROR_CODE_TAG_NAME = "errorCode"; - static final String COUNTRY_CODE_TAG_NAME = "countryCode"; - - /** - * @deprecated "region" conflicts with cloud provider region tags; prefer "regionCode" instead - */ - @Deprecated - static final String REGION_TAG_NAME = "region"; - static final String REGION_CODE_TAG_NAME = "regionCode"; - - private final String accountId; - private final String accountToken; - private final String messagingServiceSid; - private final String nanpaMessagingServiceSid; - private final String localDomain; - private final Random random; - - private final TwilioVerificationTextConfiguration defaultClientVerificationTexts; - private final Map regionalClientVerificationTexts; - - private final FaultTolerantHttpClient httpClient; - private final URI smsUri; - private final URI voxUri; - - private final DynamicConfigurationManager dynamicConfigurationManager; - - private final TwilioVerifySender twilioVerifySender; - - @VisibleForTesting - public TwilioSmsSender(String baseUri, - String baseVerifyUri, - TwilioConfiguration twilioConfiguration, - DynamicConfigurationManager dynamicConfigurationManager) { - - Executor executor = ExecutorUtils.newFixedThreadBoundedQueueExecutor(10, 100); - - this.accountId = twilioConfiguration.getAccountId(); - this.accountToken = twilioConfiguration.getAccountToken(); - this.localDomain = twilioConfiguration.getLocalDomain(); - this.messagingServiceSid = twilioConfiguration.getMessagingServiceSid(); - this.nanpaMessagingServiceSid = twilioConfiguration.getNanpaMessagingServiceSid(); - 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" ); - this.httpClient = FaultTolerantHttpClient.newBuilder() - .withCircuitBreaker(twilioConfiguration.getCircuitBreaker()) - .withRetry(twilioConfiguration.getRetry()) - .withVersion(HttpClient.Version.HTTP_2) - .withConnectTimeout(Duration.ofSeconds(10)) - .withRedirect(HttpClient.Redirect.NEVER) - .withExecutor(executor) - .withName("twilio") - .build(); - - this.defaultClientVerificationTexts = twilioConfiguration.getDefaultClientVerificationTexts(); - this.regionalClientVerificationTexts = twilioConfiguration.getRegionalClientVerificationTexts(); - - this.dynamicConfigurationManager = dynamicConfigurationManager; - this.twilioVerifySender = new TwilioVerifySender(baseVerifyUri, httpClient, twilioConfiguration); - } - - public TwilioSmsSender(TwilioConfiguration twilioConfiguration, DynamicConfigurationManager dynamicConfigurationManager) { - this("https://api.twilio.com", "https://verify.twilio.com", twilioConfiguration, dynamicConfigurationManager); - } - - public CompletableFuture deliverSmsVerification(String destination, Optional clientType, String verificationCode) { - - Map requestParameters = new HashMap<>(); - requestParameters.put("To", destination); - requestParameters.put("MessagingServiceSid", "1".equals(Util.getCountryCode(destination)) ? nanpaMessagingServiceSid : messagingServiceSid); - requestParameters.put("Body", String.format(Locale.US, getBodyFormatString(destination, clientType.orElse(null)), verificationCode)); - - HttpRequest request = HttpRequest.newBuilder() - .uri(smsUri) - .POST(FormDataBodyPublisher.of(requestParameters)) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes(StandardCharsets.UTF_8))) - .build(); - - smsMeter.mark(); - - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(this::parseResponse) - .handle((response, throwable) -> processResponse(response, throwable, destination)); - } - - 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 = verificationTexts.getIosText(); - } else if ("android-ng".equals(clientType)) { - result = verificationTexts.getAndroidNgText(); - } else if ("android-2020-01".equals(clientType)) { - result = verificationTexts.getAndroid202001Text(); - } else if ("android-2021-03".equals(clientType)) { - result = verificationTexts.getAndroid202103Text(); - } else { - result = verificationTexts.getGenericText(); - } - 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. - } else { - return result; - } - } - - public CompletableFuture deliverVoxVerification(String destination, String verificationCode, List languageRanges) { - String url = "https://" + localDomain + "/v1/voice/description/" + verificationCode; - - final String languageQueryParams = languageRanges.stream() - .map(range -> Locale.forLanguageTag(range.getRange())) - .map(locale -> { - if (StringUtils.isNotBlank(locale.getCountry())) { - return locale.getLanguage().toLowerCase() + "-" + locale.getCountry().toUpperCase(); - } else { - return locale.getLanguage().toLowerCase(); - } - }) - .map(languageTag -> "l=" + languageTag) - .collect(Collectors.joining("&")); - - if (StringUtils.isNotBlank(languageQueryParams)) { - url += "?" + languageQueryParams; - } - - Map requestParameters = new HashMap<>(); - requestParameters.put("Url", url); - requestParameters.put("To", destination); - requestParameters.put("From", getRandom(random, dynamicConfigurationManager.getConfiguration().getTwilioConfiguration().getNumbers())); - - HttpRequest request = HttpRequest.newBuilder() - .uri(voxUri) - .POST(FormDataBodyPublisher.of(requestParameters)) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Authorization", "Basic " + Base64.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes())) - .build(); - - voxMeter.mark(); - - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(this::parseResponse) - .handle((response, throwable) -> processResponse(response, throwable, destination)); - } - - private String getRandom(Random random, List elements) { - return elements.get(random.nextInt(elements.size())); - } - - private boolean processResponse(TwilioResponse response, Throwable throwable, String destination) { - if (response != null && response.isSuccess()) { - priceMeter.mark((long) (response.successResponse.price * 1000)); - return true; - } else if (response != null && response.isFailure()) { - - String countryCode = Util.getCountryCode(destination); - String region = Util.getRegion(destination); - - Metrics.counter(FAILED_REQUEST_COUNTER_NAME, - SERVICE_NAME_TAG, "classic", - STATUS_CODE_TAG_NAME, String.valueOf(response.failureResponse.status), - ERROR_CODE_TAG_NAME, String.valueOf(response.failureResponse.code), - COUNTRY_CODE_TAG_NAME, countryCode, - REGION_TAG_NAME, region, - REGION_CODE_TAG_NAME, region).increment(); - - logger.info("Failed with code={}, country={}", - response.failureResponse.code, - countryCode); - - return false; - } else if (throwable != null) { - logger.info("Twilio request failed", throwable); - return false; - } else { - logger.warn("No response or throwable!"); - return false; - } - } - - private TwilioResponse parseResponse(HttpResponse response) { - ObjectMapper mapper = SystemMapper.getMapper(); - - if (response.statusCode() >= 200 && response.statusCode() < 300) { - if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) { - return new TwilioResponse(TwilioResponse.TwilioSuccessResponse.fromBody(mapper, response.body())); - } else { - return new TwilioResponse(new TwilioResponse.TwilioSuccessResponse()); - } - } - - if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) { - return new TwilioResponse(TwilioResponse.TwilioFailureResponse.fromBody(mapper, response.body())); - } else { - return new TwilioResponse(new TwilioResponse.TwilioFailureResponse()); - } - } - - public CompletableFuture> deliverSmsVerificationWithVerify(String destination, - Optional clientType, String verificationCode, List languageRanges) { - - smsMeter.mark(); - - return twilioVerifySender.deliverSmsVerificationWithVerify(destination, clientType, verificationCode, - languageRanges); - } - - public CompletableFuture> deliverVoxVerificationWithVerify(String destination, - String verificationCode, List languageRanges) { - - voxMeter.mark(); - - return twilioVerifySender.deliverVoxVerificationWithVerify(destination, verificationCode, languageRanges); - } - - public CompletableFuture reportVerificationSucceeded(String verificationSid, @Nullable String userAgent, - String context) { - - return twilioVerifySender.reportVerificationSucceeded(verificationSid, userAgent, context); - } - - public static class TwilioResponse { - - private TwilioSuccessResponse successResponse; - private TwilioFailureResponse failureResponse; - - TwilioResponse(TwilioSuccessResponse successResponse) { - this.successResponse = successResponse; - } - - TwilioResponse(TwilioFailureResponse failureResponse) { - this.failureResponse = failureResponse; - } - - boolean isSuccess() { - return successResponse != null; - } - - boolean isFailure() { - return failureResponse != null; - } - - private static class TwilioSuccessResponse { - @JsonProperty - private double price; - - static TwilioSuccessResponse fromBody(ObjectMapper mapper, String body) { - try { - return mapper.readValue(body, TwilioSuccessResponse.class); - } catch (IOException e) { - logger.warn("Error parsing twilio success response: " + e); - return new TwilioSuccessResponse(); - } - } - } - - private static class TwilioFailureResponse { - @JsonProperty - private int status; - - @JsonProperty - private String message; - - @JsonProperty - private int code; - - static TwilioFailureResponse fromBody(ObjectMapper mapper, String body) { - try { - return mapper.readValue(body, TwilioFailureResponse.class); - } catch (IOException e) { - logger.warn("Error parsing twilio success response: " + e); - return new TwilioFailureResponse(); - } - } - } - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManager.java deleted file mode 100644 index b3cf2bf6e..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManager.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.whispersystems.textsecuregcm.sms; - -import com.google.common.annotations.VisibleForTesting; -import java.util.List; -import java.util.Locale.LanguageRange; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration; -import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; - -public class TwilioVerifyExperimentEnrollmentManager { - - @VisibleForTesting - static final String EXPERIMENT_NAME = "twilio_verify_v1"; - - private final ExperimentEnrollmentManager experimentEnrollmentManager; - - private static final Set INELIGIBLE_CLIENTS = Set.of("android-ng", "android-2020-01"); - - private final Set signalExclusiveVoiceVerificationLanguages; - - public TwilioVerifyExperimentEnrollmentManager(final VoiceVerificationConfiguration voiceVerificationConfiguration, - final ExperimentEnrollmentManager experimentEnrollmentManager) { - this.experimentEnrollmentManager = experimentEnrollmentManager; - - // Signal voice verification supports several languages that Verify does not. We want to honor - // clients that prioritize these languages, even if they would normally be enrolled in the experiment - signalExclusiveVoiceVerificationLanguages = voiceVerificationConfiguration.getLocales().stream() - .map(loc -> loc.split("-")[0]) - .filter(language -> !TwilioVerifySender.TWILIO_VERIFY_LANGUAGES.contains(language)) - .collect(Collectors.toSet()); - } - - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - public boolean isEnrolled(Optional clientType, String number, List languageRanges, - String transport) { - - final boolean clientEligible = clientType.map(client -> !INELIGIBLE_CLIENTS.contains(client)) - .orElse(true); - - final boolean languageEligible; - - if ("sms".equals(transport)) { - // Signal only sends SMS in en, while Verify supports en + many other languages - languageEligible = true; - } else { - - boolean clientPreferredLanguageOnlySupportedBySignal = false; - - for (LanguageRange languageRange : languageRanges) { - final String language = languageRange.getRange().split("-")[0]; - - if (signalExclusiveVoiceVerificationLanguages.contains(language)) { - // Support is exclusive to Signal. - // Since this is the first match in the priority list, so let's break and honor it - clientPreferredLanguageOnlySupportedBySignal = true; - break; - } - if (TwilioVerifySender.TWILIO_VERIFY_LANGUAGES.contains(language)) { - // Twilio supports it, so we can stop looping - break; - } - - // the language is supported by neither, so let's loop again - } - - languageEligible = !clientPreferredLanguageOnlySupportedBySignal; - } - final boolean enrolled = experimentEnrollmentManager.isEnrolled(number, EXPERIMENT_NAME); - - return clientEligible && languageEligible && enrolled; - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySender.java b/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySender.java deleted file mode 100644 index 610c33a61..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySender.java +++ /dev/null @@ -1,324 +0,0 @@ -package org.whispersystems.textsecuregcm.sms; - -import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Tags; -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.Base64; -import java.util.HashMap; -import java.util.List; -import java.util.Locale.LanguageRange; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nullable; -import javax.validation.constraints.NotEmpty; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; -import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; -import org.whispersystems.textsecuregcm.http.FormDataBodyPublisher; -import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; -import org.whispersystems.textsecuregcm.util.SystemMapper; -import org.whispersystems.textsecuregcm.util.Util; - -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -class TwilioVerifySender { - - private static final Logger logger = LoggerFactory.getLogger(TwilioVerifySender.class); - - private static final String VERIFICATION_SUCCEEDED_RESPONSE_COUNTER_NAME = name(TwilioVerifySender.class, - "verificationSucceeded"); - - private static final String CONTEXT_TAG_NAME = "context"; - private static final String STATUS_CODE_TAG_NAME = "statusCode"; - private static final String ERROR_CODE_TAG_NAME = "errorCode"; - - static final Set TWILIO_VERIFY_LANGUAGES = Set.of( - "af", - "ar", - "ca", - "zh", - "zh-CN", - "zh-HK", - "hr", - "cs", - "da", - "nl", - "en", - "en-GB", - "fi", - "fr", - "de", - "el", - "he", - "hi", - "hu", - "id", - "it", - "ja", - "ko", - "ms", - "nb", - "pl", - "pt", - "pt-BR", - "ro", - "ru", - "es", - "sv", - "tl", - "th", - "tr", - "vi"); - - private final String accountId; - private final String accountToken; - - private final URI verifyServiceUri; - private final URI verifyApprovalBaseUri; - private final String androidAppHash; - private final String verifyServiceFriendlyName; - private final FaultTolerantHttpClient httpClient; - - TwilioVerifySender(String baseUri, FaultTolerantHttpClient httpClient, TwilioConfiguration twilioConfiguration) { - - this.accountId = twilioConfiguration.getAccountId(); - this.accountToken = twilioConfiguration.getAccountToken(); - - this.verifyServiceUri = URI - .create(baseUri + "/v2/Services/" + twilioConfiguration.getVerifyServiceSid() + "/Verifications"); - this.verifyApprovalBaseUri = URI - .create(baseUri + "/v2/Services/" + twilioConfiguration.getVerifyServiceSid() + "/Verifications/"); - - this.androidAppHash = twilioConfiguration.getAndroidAppHash(); - this.verifyServiceFriendlyName = twilioConfiguration.getVerifyServiceFriendlyName(); - this.httpClient = httpClient; - } - - CompletableFuture> deliverSmsVerificationWithVerify(String destination, Optional clientType, - String verificationCode, List languageRanges) { - - HttpRequest request = buildVerifyRequest("sms", destination, verificationCode, findBestLocale(languageRanges), - clientType); - - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(this::parseResponse) - .handle((response, throwable) -> extractVerifySid(response, throwable, destination)); - } - - private Optional findBestLocale(List priorityList) { - return Util.findBestLocale(priorityList, TwilioVerifySender.TWILIO_VERIFY_LANGUAGES); - } - - private TwilioVerifyResponse parseResponse(HttpResponse response) { - ObjectMapper mapper = SystemMapper.getMapper(); - - if (response.statusCode() >= 200 && response.statusCode() < 300) { - if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) { - return new TwilioVerifyResponse(TwilioVerifyResponse.SuccessResponse.fromBody(mapper, response.body())); - } else { - return new TwilioVerifyResponse(new TwilioVerifyResponse.SuccessResponse()); - } - } - - if ("application/json".equals(response.headers().firstValue("Content-Type").orElse(null))) { - return new TwilioVerifyResponse(TwilioVerifyResponse.FailureResponse.fromBody(mapper, response.body())); - } else { - return new TwilioVerifyResponse(new TwilioVerifyResponse.FailureResponse()); - } - } - - CompletableFuture> deliverVoxVerificationWithVerify(String destination, - String verificationCode, List languageRanges) { - - HttpRequest request = buildVerifyRequest("call", destination, verificationCode, findBestLocale(languageRanges), - Optional.empty()); - - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(this::parseResponse) - .handle((response, throwable) -> extractVerifySid(response, throwable, destination)); - } - - private Optional extractVerifySid(TwilioVerifyResponse twilioVerifyResponse, Throwable throwable, - String destination) { - - if (throwable != null) { - logger.warn("Failed to send Twilio request", throwable); - return Optional.empty(); - } - - if (twilioVerifyResponse.isFailure()) { - String countryCode = Util.getCountryCode(destination); - String region = Util.getRegion(destination); - - Metrics.counter(TwilioSmsSender.FAILED_REQUEST_COUNTER_NAME, - TwilioSmsSender.SERVICE_NAME_TAG, "verify", - TwilioSmsSender.STATUS_CODE_TAG_NAME, String.valueOf(twilioVerifyResponse.failureResponse.status), - TwilioSmsSender.ERROR_CODE_TAG_NAME, String.valueOf(twilioVerifyResponse.failureResponse.code), - TwilioSmsSender.COUNTRY_CODE_TAG_NAME, countryCode, - TwilioSmsSender.REGION_TAG_NAME, region, - TwilioSmsSender.REGION_CODE_TAG_NAME, region).increment(); - - logger.info("Failed with code={}, country={}", - twilioVerifyResponse.failureResponse.code, - countryCode); - - return Optional.empty(); - } - - return Optional.ofNullable(twilioVerifyResponse.successResponse.getSid()); - } - - private HttpRequest buildVerifyRequest(String channel, String destination, String verificationCode, - Optional locale, Optional clientType) { - - final Map requestParameters = new HashMap<>(); - requestParameters.put("To", destination); - requestParameters.put("CustomCode", verificationCode); - requestParameters.put("Channel", channel); - requestParameters.put("CustomFriendlyName", verifyServiceFriendlyName); - locale.ifPresent(loc -> requestParameters.put("Locale", loc)); - clientType.filter(client -> client.startsWith("android")) - .ifPresent(ignored -> requestParameters.put("AppHash", androidAppHash)); - - return HttpRequest.newBuilder() - .uri(verifyServiceUri) - .POST(FormDataBodyPublisher.of(requestParameters)) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Authorization", - "Basic " + Base64.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes())) - .build(); - } - - public CompletableFuture reportVerificationSucceeded(String verificationSid, @Nullable String userAgent, - String context) { - - final Map requestParameters = new HashMap<>(); - requestParameters.put("Status", "approved"); - - HttpRequest request = HttpRequest.newBuilder() - .uri(verifyApprovalBaseUri.resolve(verificationSid)) - .POST(FormDataBodyPublisher.of(requestParameters)) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Authorization", - "Basic " + Base64.getEncoder().encodeToString((accountId + ":" + accountToken).getBytes())) - .build(); - - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .thenApply(this::parseResponse) - .handle((response, throwable) -> processVerificationSucceededResponse(response, throwable, userAgent, context)); - } - - private boolean processVerificationSucceededResponse(@Nullable final TwilioVerifyResponse response, - @Nullable final Throwable throwable, - final String userAgent, - final String context) { - - if (throwable == null) { - - assert response != null; - - final Tags tags = Tags.of(Tag.of(CONTEXT_TAG_NAME, context), UserAgentTagUtil.getPlatformTag(userAgent)); - - if (response.isSuccess() && "approved".equals(response.successResponse.getStatus())) { - // the other possible values of `status` are `pending` or `canceled`, but these can never happen in a response - // to this POST, so we don‘t consider them - Metrics.counter(VERIFICATION_SUCCEEDED_RESPONSE_COUNTER_NAME, tags) - .increment(); - - return true; - } - - // at this point, response.isFailure() == true - Metrics.counter( - VERIFICATION_SUCCEEDED_RESPONSE_COUNTER_NAME, - Tags.of(ERROR_CODE_TAG_NAME, String.valueOf(response.failureResponse.code), - STATUS_CODE_TAG_NAME, String.valueOf(response.failureResponse.status)) - .and(tags)) - .increment(); - } else { - logger.warn("Failed to send verification succeeded", throwable); - } - - return false; - } - - public static class TwilioVerifyResponse { - - private SuccessResponse successResponse; - private FailureResponse failureResponse; - - TwilioVerifyResponse(SuccessResponse successResponse) { - this.successResponse = successResponse; - } - - TwilioVerifyResponse(FailureResponse failureResponse) { - this.failureResponse = failureResponse; - } - - boolean isSuccess() { - return successResponse != null; - } - - boolean isFailure() { - return failureResponse != null; - } - - private static class SuccessResponse { - - @NotEmpty - public String sid; - - @NotEmpty - public String status; - - static SuccessResponse fromBody(ObjectMapper mapper, String body) { - try { - return mapper.readValue(body, SuccessResponse.class); - } catch (IOException e) { - logger.warn("Error parsing twilio success response: " + e); - return new SuccessResponse(); - } - } - - public String getSid() { - return sid; - } - - public String getStatus() { - return status; - } - } - - private static class FailureResponse { - - @JsonProperty - private int status; - - @JsonProperty - private String message; - - @JsonProperty - private int code; - - static FailureResponse fromBody(ObjectMapper mapper, String body) { - try { - return mapper.readValue(body, FailureResponse.class); - } catch (IOException e) { - logger.warn("Error parsing twilio response: " + e); - return new FailureResponse(); - } - } - - } - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java index 3cdb8154d..0ecfbae2e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicConfigurationTest.java @@ -209,32 +209,6 @@ class DynamicConfigurationTest { } } - @Test - void testParseTwilioConfiguration() throws JsonProcessingException { - { - final String emptyConfigYaml = REQUIRED_CONFIG.concat("test: true"); - final DynamicConfiguration emptyConfig = - DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow(); - - assertTrue(emptyConfig.getTwilioConfiguration().getNumbers().isEmpty()); - } - - { - final String twilioConfigYaml = REQUIRED_CONFIG.concat(""" - twilio: - numbers: - - 2135551212 - - 2135551313 - """); - - final DynamicTwilioConfiguration config = - DynamicConfigurationManager.parseConfiguration(twilioConfigYaml, DynamicConfiguration.class).orElseThrow() - .getTwilioConfiguration(); - - assertEquals(List.of("2135551212", "2135551313"), config.getNumbers()); - } - } - @Test void testParsePaymentsConfiguration() throws JsonProcessingException { { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java index 8984a470d..adaa31172 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/AccountControllerTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -86,7 +87,6 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse; import org.whispersystems.textsecuregcm.entities.SignedPreKey; import org.whispersystems.textsecuregcm.entities.UsernameRequest; import org.whispersystems.textsecuregcm.entities.UsernameResponse; -import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper; @@ -99,8 +99,6 @@ import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.registration.ClientType; import org.whispersystems.textsecuregcm.registration.MessageTransport; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; -import org.whispersystems.textsecuregcm.sms.SmsSender; -import org.whispersystems.textsecuregcm.sms.TwilioVerifyExperimentEnrollmentManager; import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; @@ -144,7 +142,6 @@ class AccountControllerTest { private static final String INVALID_CAPTCHA_TOKEN = "invalid_token"; private static final String TEST_NUMBER = "+14151111113"; - private static final Integer TEST_VERIFICATION_CODE = 123456; private static StoredVerificationCodeManager pendingAccountsManager = mock(StoredVerificationCodeManager.class); private static AccountsManager accountsManager = mock(AccountsManager.class); @@ -158,7 +155,6 @@ class AccountControllerTest { private static RateLimiter usernameSetLimiter = mock(RateLimiter.class); private static RateLimiter usernameReserveLimiter = mock(RateLimiter.class); private static RateLimiter usernameLookupLimiter = mock(RateLimiter.class); - private static SmsSender smsSender = mock(SmsSender.class); private static RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); private static TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); private static Account senderPinAccount = mock(Account.class); @@ -171,11 +167,6 @@ class AccountControllerTest { private static DynamicConfigurationManager dynamicConfigurationManager = mock(DynamicConfigurationManager.class); - private static TwilioVerifyExperimentEnrollmentManager verifyExperimentEnrollmentManager = mock( - TwilioVerifyExperimentEnrollmentManager.class); - - private static ExperimentEnrollmentManager experimentEnrollmentManager = mock(ExperimentEnrollmentManager.class); - private byte[] registration_lock_key = new byte[32]; private static ExternalServiceCredentialGenerator storageCredentialGenerator = new ExternalServiceCredentialGenerator(new byte[32], new byte[32], false); @@ -194,17 +185,14 @@ class AccountControllerTest { accountsManager, abusiveHostRules, rateLimiters, - smsSender, registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, - Map.of(TEST_NUMBER, TEST_VERIFICATION_CODE), + Map.of(TEST_NUMBER, 123456), recaptchaClient, pushNotificationManager, - verifyExperimentEnrollmentManager, changeNumberManager, - storageCredentialGenerator, - experimentEnrollmentManager)) + storageCredentialGenerator)) .build(); @@ -342,7 +330,6 @@ class AccountControllerTest { usernameSetLimiter, usernameReserveLimiter, usernameLookupLimiter, - smsSender, registrationServiceClient, turnTokenGenerator, senderPinAccount, @@ -351,8 +338,6 @@ class AccountControllerTest { senderTransfer, recaptchaClient, pushNotificationManager, - verifyExperimentEnrollmentManager, - experimentEnrollmentManager, changeNumberManager); clearInvocations(AuthHelper.DISABLED_DEVICE); @@ -455,7 +440,7 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(400); assertThat(response.readEntity(String.class)).isBlank(); - verifyNoMoreInteractions(pushNotificationManager); + verifyNoInteractions(pushNotificationManager); } @Test @@ -473,59 +458,17 @@ class AccountControllerTest { assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); - verifyNoMoreInteractions(pushNotificationManager); - } - - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendCode(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/sms/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header("X-Forwarded-For", NICE_HOST) - .get(); - - assertThat(response.getStatus()).isEqualTo(200); - - if (enrolledInVerifyExperiment) { - ArgumentCaptor storedVerificationCodeArgumentCaptor = ArgumentCaptor - .forClass(StoredVerificationCode.class); - - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(), eq(Collections.emptyList())); - verify(pendingAccountsManager, times(2)).store(eq(SENDER), storedVerificationCodeArgumentCaptor.capture()); - - assertThat(storedVerificationCodeArgumentCaptor.getValue().twilioVerificationSid()) - .isEqualTo("VerificationSid"); - - } else { - verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString()); - } - verifyNoMoreInteractions(smsSender); - verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); + verifyNoInteractions(pushNotificationManager); } @Test - void testSendCodeViaRegistrationService() throws NumberParseException { + void testSendCode() throws NumberParseException { final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(sessionId)); - when(experimentEnrollmentManager.isEnrolled(SENDER, AccountController.REGISTRATION_SERVICE_EXPERIMENT_NAME)) - .thenReturn(true); - Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", SENDER)) @@ -543,8 +486,6 @@ class AccountControllerTest { verify(pendingAccountsManager).store(eq(SENDER), argThat( storedVerificationCode -> Arrays.equals(storedVerificationCode.sessionId(), sessionId) && "1234-push".equals(storedVerificationCode.pushCode()))); - - verifyNoInteractions(smsSender); } @Test @@ -560,7 +501,7 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(400); assertThat(response.readEntity(String.class)).isBlank(); - verify(smsSender, never()).deliverSmsVerification(any(), any(), any()); + verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any()); } @Test @@ -581,20 +522,14 @@ class AccountControllerTest { assertThat(responseEntity.getOriginalNumber()).isEqualTo(number); assertThat(responseEntity.getNormalizedNumber()).isEqualTo("+447700900111"); - verify(smsSender, never()).deliverSmsVerification(any(), any(), any()); + verify(registrationServiceClient, never()).sendRegistrationCode(any(), any(), any(), any(), any()); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - public void testSendCodeVoiceNoLocale(final boolean enrolledInVerifyExperiment) throws Exception { + @Test + public void testSendCodeVoiceNoLocale() throws NumberParseException { - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -604,122 +539,18 @@ class AccountControllerTest { .header("X-Forwarded-For", NICE_HOST) .get(); - assertThat(response.getStatus()).isEqualTo(200); + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Collections.emptyList())); - } else { - verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Collections.emptyList())); - } + assertThat(response.getStatus()).isEqualTo(200); + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.VOICE, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - public void testSendCodeVoiceSingleLocale(final boolean enrolledInVerifyExperiment) throws Exception { + @Test + void testSendCodeWithValidPreauth() throws NumberParseException { - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } - - 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); - - if (enrolledInVerifyExperiment) { - verify(smsSender) - .deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Locale.LanguageRange.parse("pt-BR"))); - } else { - verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Locale.LanguageRange.parse("pt-BR"))); - } - verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); - } - - @ParameterizedTest - @ValueSource(booleans = {false, true}) - public void testSendCodeVoiceMultipleLocales(final boolean enrolledInVerifyExperiment) throws Exception { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } - - 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); - - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Locale.LanguageRange - .parse("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"))); - } else { - verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Locale.LanguageRange - .parse("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"))); - } - verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); - } - - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendCodeVoiceInvalidLocale(boolean enrolledInVerifyExperiment) throws Exception { - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverVoxVerificationWithTwilioVerify(anyString(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } - - Response response = - resources.getJerseyTest() - .target(String.format("/v1/accounts/voice/code/%s", SENDER)) - .queryParam("challenge", "1234-push") - .request() - .header("Accept-Language", "This is not a reasonable Accept-Language value") - .header("X-Forwarded-For", NICE_HOST) - .get(); - - // Should still send a code, just with no accept language - assertThat(response.getStatus()).isEqualTo(200); - - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverVoxVerificationWithTwilioVerify(eq(SENDER), anyString(), eq(Collections.emptyList())); - } else { - verify(smsSender).deliverVoxVerification(eq(SENDER), anyString(), eq(Collections.emptyList())); - } - } - - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendCodeWithValidPreauth(final boolean enrolledInVerifyExperiment) throws Exception { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -731,17 +562,14 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString(), - eq(Collections.emptyList())); - } else { - verify(smsSender).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString()); - } + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER_PREAUTH, null); + + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); } @Test - void testSendCodeWithInvalidPreauth() throws Exception { + void testSendCodeWithInvalidPreauth() { Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) @@ -752,12 +580,12 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(403); - verifyNoMoreInteractions(smsSender); - verifyNoMoreInteractions(abusiveHostRules); + verifyNoInteractions(registrationServiceClient); + verifyNoInteractions(abusiveHostRules); } @Test - void testSendCodeWithNoPreauth() throws Exception { + void testSendCodeWithNoPreauth() { Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) @@ -767,22 +595,14 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(402); - verify(smsSender, never()).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString()); - verify(smsSender, never()).deliverSmsVerificationWithTwilioVerify(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString(), anyList()); + verifyNoInteractions(registrationServiceClient); } + @Test + void testSendiOSCode() throws NumberParseException { - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendiOSCode(final boolean enrolledInVerifyExperiment) throws Exception { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -795,25 +615,15 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.of("ios")), anyString(), - eq(Collections.emptyList())); - } else { - verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("ios")), anyString()); - } + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null); + + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.IOS, null, AccountController.REGISTRATION_RPC_TIMEOUT); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendAndroidNgCode(final boolean enrolledInVerifyExperiment) throws Exception { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + @Test + void testSendAndroidNgCode() throws NumberParseException { + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -826,25 +636,13 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.of("android-ng")), anyString(), - eq(Collections.emptyList())); - } else { - verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.of("android-ng")), anyString()); - } + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null); + + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.ANDROID_WITHOUT_FCM, null, AccountController.REGISTRATION_RPC_TIMEOUT); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendAbusiveHost(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + @Test + void testSendAbusiveHost() { Response response = resources.getJerseyTest() @@ -857,20 +655,14 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(402); verify(abusiveHostRules).isBlocked(eq(ABUSIVE_HOST)); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(registrationServiceClient); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendAbusiveHostWithValidCaptcha(final boolean enrolledInVerifyExperiment) throws IOException { + @Test + void testSendAbusiveHostWithValidCaptcha() throws NumberParseException { - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -882,27 +674,16 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); - verifyNoMoreInteractions(abusiveHostRules); + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null); + + verifyNoInteractions(abusiveHostRules); verify(recaptchaClient).verify(eq(VALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST)); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(), - eq(Collections.emptyList())); - } else { - verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString()); - } + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendAbusiveHostWithInvalidCaptcha(final boolean enrolledInVerifyExperiment) { + @Test + void testSendAbusiveHostWithInvalidCaptcha() { - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", SENDER)) @@ -913,23 +694,13 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(402); - verifyNoMoreInteractions(abusiveHostRules); + verifyNoInteractions(abusiveHostRules); verify(recaptchaClient).verify(eq(INVALID_CAPTCHA_TOKEN), eq(ABUSIVE_HOST)); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(registrationServiceClient); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendRateLimitedHostAutoBlock(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } - + @Test + void testSendRateLimitedHostAutoBlock() { Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", SENDER)) @@ -944,21 +715,12 @@ class AccountControllerTest { verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_IP_HOST)); verifyNoMoreInteractions(abusiveHostRules); - verifyNoMoreInteractions(recaptchaClient); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(recaptchaClient); + verifyNoInteractions(registrationServiceClient); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendRateLimitedPrefixAutoBlock(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + @Test + void testSendRateLimitedPrefixAutoBlock() { Response response = resources.getJerseyTest() @@ -974,21 +736,12 @@ class AccountControllerTest { verify(abusiveHostRules).setBlockedHost(eq(RATE_LIMITED_PREFIX_HOST)); verifyNoMoreInteractions(abusiveHostRules); - verifyNoMoreInteractions(recaptchaClient); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(recaptchaClient); + verifyNoInteractions(registrationServiceClient); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendRateLimitedHostNoAutoBlock(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } + @Test + void testSendRateLimitedHostNoAutoBlock() { Response response = resources.getJerseyTest() @@ -1003,17 +756,13 @@ class AccountControllerTest { verify(abusiveHostRules).isBlocked(eq(RATE_LIMITED_HOST2)); verifyNoMoreInteractions(abusiveHostRules); - verifyNoMoreInteractions(recaptchaClient); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(recaptchaClient); + verifyNoInteractions(registrationServiceClient); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendMultipleHost(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); + @Test + void testSendMultipleHost() { Response response = resources.getJerseyTest() @@ -1028,16 +777,12 @@ class AccountControllerTest { verify(abusiveHostRules, times(1)).isBlocked(eq(ABUSIVE_HOST)); verifyNoMoreInteractions(abusiveHostRules); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(registrationServiceClient); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendRestrictedHostOut(final boolean enrolledInVerifyExperiment) { - - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); + @Test + void testSendRestrictedHostOut() { final String challenge = "challenge"; when(pendingAccountsManager.getCodeForNumber(RESTRICTED_NUMBER)) @@ -1054,17 +799,15 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(402); verify(abusiveHostRules).isBlocked(eq(NICE_HOST)); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(registrationServiceClient); } @ParameterizedTest @CsvSource({ - "+12025550123, true, true", - "+12025550123, false, true", - "+12505550199, true, false", - "+12505550199, false, false", + "+12025550123, true", + "+12505550199, false", }) - void testRestrictedRegion(final String number, final boolean enrolledInVerifyExperiment, final boolean expectSendCode) { + void testRestrictedRegion(final String number, final boolean expectSendCode) throws NumberParseException { final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); @@ -1073,15 +816,12 @@ class AccountControllerTest { when(dynamicConfiguration.getCaptchaConfiguration()).thenReturn(signupCaptchaConfig); - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - final String challenge = "challenge"; when(pendingAccountsManager.getCodeForNumber(number)) .thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null))); - when(smsSender.deliverSmsVerificationWithTwilioVerify(any(), any(), any(), any())) - .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -1094,31 +834,22 @@ class AccountControllerTest { if (expectSendCode) { assertThat(response.getStatus()).isEqualTo(200); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(number), any(), any(), any()); - } else { - verify(smsSender).deliverSmsVerification(eq(number), any(), any()); - } + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(number, null); + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); } else { assertThat(response.getStatus()).isEqualTo(402); - verifyNoMoreInteractions(smsSender); + verifyNoInteractions(registrationServiceClient); } } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testSendRestrictedIn(final boolean enrolledInVerifyExperiment) throws Exception { + @Test + void testSendRestrictedIn() throws NumberParseException { - when(verifyExperimentEnrollmentManager.isEnrolled(any(), anyString(), anyList(), anyString())) - .thenReturn(enrolledInVerifyExperiment); - - if (enrolledInVerifyExperiment) { - when(smsSender.deliverSmsVerificationWithTwilioVerify(anyString(), any(), anyString(), anyList())) - .thenReturn(CompletableFuture.completedFuture(Optional.of("VerificationSid"))); - } final String challenge = "challenge"; when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("123456", System.currentTimeMillis(), challenge, null, null))); + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); Response response = resources.getJerseyTest() @@ -1130,40 +861,38 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); - if (enrolledInVerifyExperiment) { - verify(smsSender).deliverSmsVerificationWithTwilioVerify(eq(SENDER), eq(Optional.empty()), anyString(), - eq(Collections.emptyList())); - } else { - verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString()); - } + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null); - verifyNoMoreInteractions(smsSender); + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); } @Test - void testSendCodeTestDeviceNumber() throws Exception { - // no push code and a blocked host, but should evade captchas and skip smsSender + void testSendCodeTestDeviceNumber() throws NumberParseException { + final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); + + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(sessionId)); + Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", TEST_NUMBER)) .request() .header("X-Forwarded-For", ABUSIVE_HOST) .get(); - ArgumentCaptor captor = ArgumentCaptor.forClass(StoredVerificationCode.class); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(StoredVerificationCode.class); verify(pendingAccountsManager).store(eq(TEST_NUMBER), captor.capture()); - assertThat(captor.getValue().code()).isEqualTo(Integer.toString(TEST_VERIFICATION_CODE)); + assertThat(captor.getValue().code()).isNull(); + assertThat(captor.getValue().sessionId()).isEqualTo(sessionId); assertThat(response.getStatus()).isEqualTo(200); - verifyNoInteractions(smsSender); + + // Even though no actual SMS will be sent, we leave that decision to the registration service + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(TEST_NUMBER, null); + verify(registrationServiceClient).sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testVerifyCode(final boolean enrolledInVerifyExperiment) throws Exception { - if (enrolledInVerifyExperiment) { - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn( - Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), "1234-push", "VerificationSid", null))); - } - + @Test + void testVerifyCode() throws Exception { resources.getJerseyTest() .target(String.format("/v1/accounts/code/%s", "1234")) .request() @@ -1171,11 +900,6 @@ class AccountControllerTest { .put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList()); - - if (enrolledInVerifyExperiment) { - verify(smsSender).reportVerificationSucceeded(eq("VerificationSid"), any(), eq("registration")); - } - verifyNoInteractions(registrationServiceClient); } @@ -1199,7 +923,6 @@ class AccountControllerTest { verify(accountsManager).create(eq(SENDER), eq("bar"), any(), any(), anyList()); verify(registrationServiceClient).checkVerificationCode(sessionId, "1234", AccountController.REGISTRATION_RPC_TIMEOUT); - verifyNoInteractions(smsSender); } @Test @@ -1225,7 +948,7 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(403); - verifyNoMoreInteractions(accountsManager); + verifyNoInteractions(accountsManager); } @Test @@ -1240,7 +963,7 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(403); - verifyNoMoreInteractions(accountsManager); + verifyNoInteractions(accountsManager); } @Test @@ -1266,7 +989,6 @@ class AccountControllerTest { verify(registrationServiceClient).checkVerificationCode(sessionId, "1111", AccountController.REGISTRATION_RPC_TIMEOUT); verifyNoInteractions(accountsManager); - verifyNoInteractions(smsSender); } @Test @@ -1320,7 +1042,7 @@ class AccountControllerTest { assertThat(result.getUuid()).isNotNull(); - verifyNoMoreInteractions(pinLimiter); + verifyNoInteractions(pinLimiter); } finally { when(senderRegLockAccount.getRegistrationLock()).thenReturn(lock); } @@ -1361,7 +1083,7 @@ class AccountControllerTest { assertThat(failure.getBackupCredentials().getPassword().startsWith(SENDER_REG_LOCK_UUID.toString())).isTrue(); assertThat(failure.getTimeRemaining()).isGreaterThan(0); - verifyNoMoreInteractions(pinLimiter); + verifyNoInteractions(pinLimiter); } @Test @@ -1411,55 +1133,14 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); } - @Test - void testVerifyTestDeviceNumber() throws Exception { - - when(pendingAccountsManager.getCodeForNumber(TEST_NUMBER)).thenReturn(Optional.of( - new StoredVerificationCode(Integer.toString(TEST_VERIFICATION_CODE), System.currentTimeMillis(), "push", null, null))); - - - final Response response = resources.getJerseyTest() - .target(String.format("/v1/accounts/code/%s", TEST_VERIFICATION_CODE)) - .request() - .header("Authorization", AuthHelper.getProvisioningAuthHeader(TEST_NUMBER, "bar")) - .put(Entity.entity(new AccountAttributes(), MediaType.APPLICATION_JSON_TYPE)); - - verify(accountsManager).create(eq(TEST_NUMBER), eq("bar"), any(), any(), anyList()); - assertThat(response.getStatus()).isEqualTo(200); - - } - @Test void testChangePhoneNumber() throws Exception { final String number = "+18005559876"; final String code = "987654"; - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); - - final AccountIdentityResponse accountIdentityResponse = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code, null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class); - - verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(number), any(), any(), any(), any()); - - assertThat(accountIdentityResponse.getUuid()).isEqualTo(AuthHelper.VALID_UUID); - assertThat(accountIdentityResponse.getNumber()).isEqualTo(number); - assertThat(accountIdentityResponse.getPni()).isNotEqualTo(AuthHelper.VALID_PNI); - } - - @Test - void testChangePhoneNumberWithRegistrationService() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; final byte[] sessionId = "session".getBytes(StandardCharsets.UTF_8); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, sessionId))); + new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId))); when(registrationServiceClient.checkVerificationCode(any(), any(), any())) .thenReturn(CompletableFuture.completedFuture(true)); @@ -1557,26 +1238,6 @@ class AccountControllerTest { void testChangePhoneNumberIncorrectCode() throws Exception { final String number = "+18005559876"; final String code = "987654"; - - when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); - - final Response response = - resources.getJerseyTest() - .target("/v1/accounts/number") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new ChangePhoneNumberRequest(number, code + "-incorrect", null, null, null, null, null), - MediaType.APPLICATION_JSON_TYPE)); - - assertThat(response.getStatus()).isEqualTo(403); - verify(changeNumberManager, never()).changeNumber(any(), any(), any(), any(), any(), any()); - } - - @Test - void testChangePhoneNumberIncorrectCodeWithRegistrationService() throws Exception { - final String number = "+18005559876"; - final String code = "987654"; final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( @@ -1603,9 +1264,13 @@ class AccountControllerTest { void testChangePhoneNumberExistingAccountReglockNotRequired() throws Exception { final String number = "+18005559876"; final String code = "987654"; + final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); + new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, sessionId))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(false); @@ -1633,9 +1298,13 @@ class AccountControllerTest { void testChangePhoneNumberExistingAccountReglockRequiredNotProvided() throws Exception { final String number = "+18005559876"; final String code = "987654"; + final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); + new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, sessionId))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); @@ -1664,9 +1333,13 @@ class AccountControllerTest { final String number = "+18005559876"; final String code = "987654"; final String reglock = "setec-astronomy"; + final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); + new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); @@ -1696,9 +1369,13 @@ class AccountControllerTest { final String number = "+18005559876"; final String code = "987654"; final String reglock = "setec-astronomy"; + final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); + new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); final StoredRegistrationLock existingRegistrationLock = mock(StoredRegistrationLock.class); when(existingRegistrationLock.requiresClientRegistrationLock()).thenReturn(true); @@ -1728,6 +1405,7 @@ class AccountControllerTest { final String number = "+18005559876"; final String code = "987654"; final String pniIdentityKey = "changed-pni-identity-key"; + final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8); Device device2 = mock(Device.class); when(device2.getId()).thenReturn(2L); @@ -1744,7 +1422,10 @@ class AccountControllerTest { when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3)); when(pendingAccountsManager.getCodeForNumber(number)).thenReturn(Optional.of( - new StoredVerificationCode(code, System.currentTimeMillis(), "push", null, null))); + new StoredVerificationCode(null, System.currentTimeMillis(), "push", null, sessionId))); + + when(registrationServiceClient.checkVerificationCode(any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(true)); var deviceMessages = List.of( new IncomingMessage(1, 2, 2, "content2"), @@ -2104,7 +1785,8 @@ class AccountControllerTest { @ParameterizedTest @MethodSource - void testSignupCaptcha(final String message, final boolean enforced, final Set countryCodes, final int expectedResponseStatusCode) { + void testSignupCaptcha(final String message, final boolean enforced, final Set countryCodes, final int expectedResponseStatusCode) + throws NumberParseException { DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); when(dynamicConfigurationManager.getConfiguration()) .thenReturn(dynamicConfiguration); @@ -2114,6 +1796,9 @@ class AccountControllerTest { when(dynamicConfiguration.getCaptchaConfiguration()) .thenReturn(signupCaptchaConfig); + when(registrationServiceClient.sendRegistrationCode(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(new byte[16])); + Response response = resources.getJerseyTest() .target(String.format("/v1/accounts/sms/code/%s", SENDER)) @@ -2124,8 +1809,10 @@ class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(expectedResponseStatusCode); - verify(smsSender, 200 == expectedResponseStatusCode ? times(1) : never()) - .deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString()); + final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtil.getInstance().parse(SENDER, null); + + verify(registrationServiceClient, 200 == expectedResponseStatusCode ? times(1) : never()) + .sendRegistrationCode(phoneNumber, MessageTransport.SMS, ClientType.UNKNOWN, null, AccountController.REGISTRATION_RPC_TIMEOUT); } static Stream testSignupCaptcha() { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManagerTest.java deleted file mode 100644 index c57936912..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifyExperimentEnrollmentManagerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.whispersystems.textsecuregcm.sms; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.List; -import java.util.Locale.LanguageRange; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.whispersystems.textsecuregcm.configuration.VoiceVerificationConfiguration; -import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; - -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") -class TwilioVerifyExperimentEnrollmentManagerTest { - - private final ExperimentEnrollmentManager experimentEnrollmentManager = mock(ExperimentEnrollmentManager.class); - private final VoiceVerificationConfiguration voiceVerificationConfiguration = mock(VoiceVerificationConfiguration.class); - private TwilioVerifyExperimentEnrollmentManager manager; - - private static final String NUMBER = "+15055551212"; - - private static final Optional INELIGIBLE_CLIENT = Optional.of("android-2020-01"); - private static final Optional ELIGIBLE_CLIENT = Optional.of("anything"); - - private static final List LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL = LanguageRange.parse("am"); - private static final List LANGUAGE_NOT_SUPPORTED_BY_SIGNAL_OR_TWILIO = LanguageRange.parse("xx"); - private static final List LANGUAGE_SUPPORTED_BY_TWILIO = LanguageRange.parse("fr-CA"); - - - @BeforeEach - void setup() { - when(voiceVerificationConfiguration.getLocales()) - .thenReturn(Set.of("am", "en-US", "fr-CA")); - - manager = new TwilioVerifyExperimentEnrollmentManager( - voiceVerificationConfiguration, - experimentEnrollmentManager); - } - - @ParameterizedTest - @MethodSource - void testIsEnrolled(String message, boolean expected, Optional clientType, String number, - List languageRanges, String transport, boolean managerResponse) { - - when(experimentEnrollmentManager.isEnrolled(number, TwilioVerifyExperimentEnrollmentManager.EXPERIMENT_NAME)) - .thenReturn(managerResponse); - assertEquals(expected, manager.isEnrolled(clientType, number, languageRanges, transport), message); - } - - static Stream testIsEnrolled() { - return Stream.of( - Arguments.of("ineligible client", false, INELIGIBLE_CLIENT, NUMBER, Collections.emptyList(), "sms", true), - Arguments - .of("ineligible client", false, Optional.of("android-ng"), NUMBER, Collections.emptyList(), "sms", true), - Arguments - .of("client, language, and manager all agree on enrollment", true, ELIGIBLE_CLIENT, NUMBER, - LANGUAGE_SUPPORTED_BY_TWILIO, - "sms", true), - - Arguments - .of("enrolled: ineligible language doesn’t matter with sms", true, ELIGIBLE_CLIENT, NUMBER, - LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL, "sms", - true), - - Arguments - .of("not enrolled: language only supported by Signal is preferred", false, ELIGIBLE_CLIENT, NUMBER, List.of( - LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL.get(0), LANGUAGE_SUPPORTED_BY_TWILIO.get(0)), "voice", true), - - Arguments.of("enrolled: preferred language is supported", true, ELIGIBLE_CLIENT, NUMBER, List.of( - LANGUAGE_SUPPORTED_BY_TWILIO.get(0), LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL - .get(0)), "voice", true), - - Arguments - .of("enrolled: preferred (and only) language is not supported by Signal or Twilio", true, ELIGIBLE_CLIENT, - NUMBER, LANGUAGE_NOT_SUPPORTED_BY_SIGNAL_OR_TWILIO, "voice", true), - - Arguments.of("not enrolled: preferred language (and only) is only supported by Siganl", false, ELIGIBLE_CLIENT, - NUMBER, LANGUAGE_ONLY_SUPPORTED_BY_SIGNAL, "voice", true) - - ); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySenderTest.java deleted file mode 100644 index caa6f6e5a..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/sms/TwilioVerifySenderTest.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright 2021-2022 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.sms; - -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.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.core.WireMockConfiguration.wireMockConfig; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.github.tomakehurst.wiremock.junit5.WireMockExtension; -import java.net.http.HttpClient; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Locale.LanguageRange; -import java.util.Optional; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; -import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; -import org.whispersystems.textsecuregcm.util.ExecutorUtils; - -@SuppressWarnings("OptionalGetWithoutIsPresent") -class TwilioVerifySenderTest { - - private static final String ACCOUNT_ID = "test_account_id"; - private static final String ACCOUNT_TOKEN = "test_account_token"; - 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 VERIFY_SERVICE_SID = "verify_service_sid"; - private static final String LOCAL_DOMAIN = "test.com"; - private static final String ANDROID_APP_HASH = "someHash"; - private static final String SERVICE_FRIENDLY_NAME = "SignalTest"; - - private static final String VERIFICATION_SID = "verification"; - - @RegisterExtension - private final WireMockExtension wireMock = WireMockExtension.newInstance() - .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) - .build(); - - private TwilioVerifySender sender; - - @BeforeEach - void setup() { - final TwilioConfiguration twilioConfiguration = createTwilioConfiguration(); - - final FaultTolerantHttpClient httpClient = FaultTolerantHttpClient.newBuilder() - .withCircuitBreaker(twilioConfiguration.getCircuitBreaker()) - .withRetry(twilioConfiguration.getRetry()) - .withVersion(HttpClient.Version.HTTP_2) - .withConnectTimeout(Duration.ofSeconds(10)) - .withRedirect(HttpClient.Redirect.NEVER) - .withExecutor(ExecutorUtils.newFixedThreadBoundedQueueExecutor(10, 100)) - .withName("twilio") - .build(); - - sender = new TwilioVerifySender("http://localhost:" + wireMock.getPort(), httpClient, twilioConfiguration); - } - - private TwilioConfiguration createTwilioConfiguration() { - - TwilioConfiguration configuration = new TwilioConfiguration(); - - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setMessagingServiceSid(MESSAGING_SERVICE_SID); - configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID); - configuration.setVerifyServiceSid(VERIFY_SERVICE_SID); - configuration.setLocalDomain(LOCAL_DOMAIN); - configuration.setAndroidAppHash(ANDROID_APP_HASH); - configuration.setVerifyServiceFriendlyName(SERVICE_FRIENDLY_NAME); - - return configuration; - } - - private void setupSuccessStubForVerify() { - wireMock.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody("{\"sid\": \"" + VERIFICATION_SID + "\", \"status\": \"pending\"}"))); - } - - @ParameterizedTest - @MethodSource - void deliverSmsVerificationWithVerify(@Nullable final String client, @Nullable final String languageRange, - final boolean expectAppHash, @Nullable final String expectedLocale) throws Exception { - - setupSuccessStubForVerify(); - - List languageRanges = Optional.ofNullable(languageRange) - .map(LanguageRange::parse) - .orElse(Collections.emptyList()); - - final Optional verificationSid = sender - .deliverSmsVerificationWithVerify("+14153333333", Optional.ofNullable(client), "123456", - languageRanges).get(); - - assertEquals(VERIFICATION_SID, verificationSid.get()); - - wireMock.verify(1, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(equalTo( - (expectedLocale == null ? "" : "Locale=" + expectedLocale + "&") - + "Channel=sms&To=%2B14153333333&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME - + "&CustomCode=123456" + (expectAppHash ? "&AppHash=" + ANDROID_APP_HASH : "") - ))); - } - - @SuppressWarnings("unused") - private static Stream deliverSmsVerificationWithVerify() { - return Stream.of( - // client, languageRange, expectAppHash, expectedLocale - Arguments.of("ios", "fr-CA, en", false, "fr"), - Arguments.of("android-2021-03", "zh-HK, it", true, "zh-HK"), - Arguments.of(null, null, false, null) - ); - } - - @ParameterizedTest - @MethodSource - void deliverVoxVerificationWithVerify(@Nullable final String languageRange, - @Nullable final String expectedLocale) throws Exception { - - setupSuccessStubForVerify(); - - final List languageRanges = Optional.ofNullable(languageRange) - .map(LanguageRange::parse) - .orElse(Collections.emptyList()); - - final Optional verificationSid = sender - .deliverVoxVerificationWithVerify("+14153333333", "123456", languageRanges).get(); - - assertEquals(VERIFICATION_SID, verificationSid.get()); - - wireMock.verify(1, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(equalTo( - (expectedLocale == null ? "" : "Locale=" + expectedLocale + "&") - + "Channel=call&To=%2B14153333333&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME - + "&CustomCode=123456"))); - } - - @SuppressWarnings("unused") - private static Stream deliverVoxVerificationWithVerify() { - return Stream.of( - // languageRange, expectedLocale - Arguments.of("fr-CA, en", "fr"), - Arguments.of("zh-HK, it", "zh-HK"), - Arguments.of("en-CAA, en", "en"), - Arguments.of(null, null) - ); - } - - @Test - void testSmsFiveHundred() throws Exception { - wireMock.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"message\": \"Server error!\"}"))); - - final Optional verificationSid = sender - .deliverSmsVerificationWithVerify("+14153333333", Optional.empty(), "123456", Collections.emptyList()).get(); - - assertThat(verificationSid).isEmpty(); - - wireMock.verify(3, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(equalTo("Channel=sms&To=%2B14153333333&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME - + "&CustomCode=123456"))); - } - - @Test - void testVoxFiveHundred() throws Exception { - wireMock.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"message\": \"Server error!\"}"))); - - final Optional verificationSid = sender - .deliverVoxVerificationWithVerify("+14153333333", "123456", Collections.emptyList()).get(); - - assertThat(verificationSid).isEmpty(); - - wireMock.verify(3, postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications")) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(equalTo("Channel=call&To=%2B14153333333&CustomFriendlyName=" + SERVICE_FRIENDLY_NAME - + "&CustomCode=123456"))); - } - - @Test - void reportVerificationSucceeded() throws Exception { - - wireMock.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID)) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\": \"approved\", \"sid\": \"" + VERIFICATION_SID + "\"}"))); - - final Boolean success = sender.reportVerificationSucceeded(VERIFICATION_SID, null, "test").get(); - - assertThat(success).isTrue(); - - wireMock.verify(1, - postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID)) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(equalTo("Status=approved"))); - } - - @Test - void reportVerificationFailed() throws Exception { - - wireMock.stubFor(post(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID)) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\": 404, \"code\": 20404}"))); - - final Boolean success = sender.reportVerificationSucceeded(VERIFICATION_SID, null, "test").get(); - - assertThat(success).isFalse(); - - wireMock.verify(1, - postRequestedFor(urlEqualTo("/v2/Services/" + VERIFY_SERVICE_SID + "/Verifications/" + VERIFICATION_SID)) - .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) - .withRequestBody(equalTo("Status=approved"))); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/SmsSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/SmsSenderTest.java deleted file mode 100644 index f2e191f4d..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/SmsSenderTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.whispersystems.textsecuregcm.tests.sms; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.times; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.whispersystems.textsecuregcm.sms.SmsSender; -import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; - -class SmsSenderTest { - - private static final String NON_MEXICO_NUMBER = "+12345678901"; - private static final String MEXICO_NON_MOBILE_NUMBER = "+52234567890"; - private static final String MEXICO_MOBILE_NUMBER = "+52123456789"; - - private final TwilioSmsSender twilioSmsSender = mock(TwilioSmsSender.class); - private final SmsSender smsSender = new SmsSender(twilioSmsSender); - - @Test - void testDeliverSmsVerificationNonMexico() { - smsSender.deliverSmsVerification(NON_MEXICO_NUMBER, Optional.empty(), ""); - verify(twilioSmsSender, times(1)) - .deliverSmsVerification(NON_MEXICO_NUMBER, Optional.empty(), ""); - } - - @Test - void testDeliverSmsVerificationMexicoNonMobile() { - smsSender.deliverSmsVerification(MEXICO_NON_MOBILE_NUMBER, Optional.empty(), ""); - verify(twilioSmsSender, times(1)) - .deliverSmsVerification("+521" + MEXICO_NON_MOBILE_NUMBER.substring("+52".length()), Optional.empty(), ""); - } - - @Test - void testDeliverSmsVerificationMexicoMobile() { - smsSender.deliverSmsVerification(MEXICO_MOBILE_NUMBER, Optional.empty(), ""); - verify(twilioSmsSender, times(1)) - .deliverSmsVerification(MEXICO_MOBILE_NUMBER, Optional.empty(), ""); - } -} 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 deleted file mode 100644 index 6841b3a01..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/sms/TwilioSmsSenderTest.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2013-2020 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.tests.sms; - -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.core.WireMockConfiguration.wireMockConfig; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.github.tomakehurst.wiremock.junit5.WireMockExtension; -import java.util.List; -import java.util.Locale.LanguageRange; -import java.util.Map; -import java.util.Optional; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -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; -import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; - -class TwilioSmsSenderTest { - - private static final String ACCOUNT_ID = "test_account_id"; - private static final String ACCOUNT_TOKEN = "test_account_token"; - 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 VERIFY_SERVICE_SID = "verify_service_sid"; - private static final String LOCAL_DOMAIN = "test.com"; - - @RegisterExtension - private final WireMockExtension wireMock = WireMockExtension.newInstance() - .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) - .build(); - - private DynamicConfigurationManager dynamicConfigurationManager; - private TwilioSmsSender sender; - - @BeforeEach - void setup() { - - dynamicConfigurationManager = mock(DynamicConfigurationManager.class); - DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(); - DynamicTwilioConfiguration dynamicTwilioConfiguration = new DynamicTwilioConfiguration(); - dynamicConfiguration.setTwilioConfiguration(dynamicTwilioConfiguration); - dynamicTwilioConfiguration.setNumbers(List.of("+14151111111", "+14152222222")); - when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); - - TwilioConfiguration configuration = createTwilioConfiguration(); - sender = new TwilioSmsSender("http://localhost:" + wireMock.getPort(), "http://localhost:11111", configuration, dynamicConfigurationManager); - } - - @Nonnull - private TwilioConfiguration createTwilioConfiguration() { - TwilioConfiguration configuration = new TwilioConfiguration(); - configuration.setAccountId(ACCOUNT_ID); - configuration.setAccountToken(ACCOUNT_TOKEN); - configuration.setMessagingServiceSid(MESSAGING_SERVICE_SID); - configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID); - configuration.setVerifyServiceSid(VERIFY_SERVICE_SID); - configuration.setLocalDomain(LOCAL_DOMAIN); - - 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() { - wireMock.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 - void testSendSms() { - setupSuccessStubForSms(); - - boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); - - assertThat(success).isTrue(); - - wireMock.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=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters"))); - } - - @Test - void testSendSmsAndroid202001() { - setupSuccessStubForSms(); - - boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-2020-01"), "123-456").join(); - - assertThat(success).isTrue(); - - wireMock.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+Android202001%3A+123-456%0A%0Asomelink%3A%2F%2Fverify%2F123-456%0A%0Acharacters"))); - } - - @Test - void testSendSmsAndroid202103() { - setupSuccessStubForSms(); - - boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-2021-03"), "123456").join(); - - assertThat(success).isTrue(); - - wireMock.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+Android202103%3A+123456%0A%0Acharacters"))); - } - - @Test - void testSendSmsNanpaMessagingService() { - setupSuccessStubForSms(); - TwilioConfiguration configuration = createTwilioConfiguration(); - configuration.setNanpaMessagingServiceSid(NANPA_MESSAGING_SERVICE_SID); - - TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + wireMock.getPort(), - "http://localhost:11111", configuration, dynamicConfigurationManager); - - assertThat(sender.deliverSmsVerification("+14153333333", Optional.of("ios"), "654-321").join()).isTrue(); - wireMock.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"))); - - wireMock.resetRequests(); - assertThat(sender.deliverSmsVerification("+447911123456", Optional.of("ios"), "654-321").join()).isTrue(); - wireMock.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 - void testSendVox() { - wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody("{\"price\": -0.00750, \"status\": \"completed\"}"))); - - boolean success = sender.deliverVoxVerification("+14153333333", "123-456", LanguageRange.parse("en-US")).join(); - - assertThat(success).isTrue(); - - wireMock.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"))); - } - - @Test - void testSendVoxMultipleLocales() { - wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withHeader("Content-Type", "application/json") - .withBody("{\"price\": -0.00750, \"status\": \"completed\"}"))); - - boolean success = sender.deliverVoxVerification("+14153333333", "123-456", LanguageRange.parse("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")).join(); - - assertThat(success).isTrue(); - - wireMock.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%26l%3Dar-US%26l%3Dfa-US%26l%3Dzh-US%26l%3Dru-RU%26l%3Dzh-US"))); - } - - @Test - void testSendSmsFiveHundred() { - wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"message\": \"Server error!\"}"))); - - boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); - - assertThat(success).isFalse(); - - wireMock.verify(3, 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=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters"))); - } - - @Test - void testSendVoxFiveHundred() { - wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Calls.json")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"message\": \"Server error!\"}"))); - - boolean success = sender.deliverVoxVerification("+14153333333", "123-456", LanguageRange.parse("en-US")).join(); - - assertThat(success).isFalse(); - - wireMock.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"))); - - } - - @Test - void testSendSmsNetworkFailure() { - TwilioConfiguration configuration = createTwilioConfiguration(); - TwilioSmsSender sender = new TwilioSmsSender("http://localhost:" + 39873, "http://localhost:" + 39873, configuration, dynamicConfigurationManager); - - boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); - - assertThat(success).isFalse(); - } - - @Test - void testRetrySmsOnUnreachableErrorCodeIsTriedOnlyOnceWithoutSenderId() { - wireMock.stubFor(post(urlEqualTo("/2010-04-01/Accounts/" + ACCOUNT_ID + "/Messages.json")) - .withBasicAuth(ACCOUNT_ID, ACCOUNT_TOKEN) - .willReturn(aResponse() - .withStatus(400) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\": 400, \"message\": \"is not currently reachable\", \"code\": 21612}"))); - - boolean success = sender.deliverSmsVerification("+14153333333", Optional.of("android-ng"), "123-456").join(); - - assertThat(success).isFalse(); - - wireMock.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=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters"))); - } - - @Test - void testSendSmsChina() { - setupSuccessStubForSms(); - - boolean success = sender.deliverSmsVerification("+861065529988", Optional.of("android-ng"), "123-456").join(); - - assertThat(success).isTrue(); - - wireMock.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=%2B861065529988&Body=%3C%23%3E+Verify+on+AndroidNg%3A+123-456%0A%0Acharacters%E2%80%88"))); - } - - @Test - void testSendSmsRegionalVerificationText() { - setupSuccessStubForSms(); - - boolean success = sender.deliverSmsVerification("+33655512673", Optional.of("android-ng"), "123-456").join(); - - assertThat(success).isTrue(); - - wireMock.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"))); - } - -}