From 48a81da883fe116562069a2e33e43d6d0385e48d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 27 Oct 2021 11:09:17 -0400 Subject: [PATCH] Handle non-normalized phone number responses. --- ...equestVerificationCodeResponseProcessor.kt | 29 +++++++ .../fragments/EnterPhoneNumberFragment.java | 45 ++++++++++- .../thoughtcrime/securesms/util/Dialogs.java | 25 +++--- app/src/main/res/values/strings.xml | 3 + .../ImpossiblePhoneNumberException.java | 10 +++ .../NonNormalizedPhoneNumberException.java | 46 +++++++++++ .../internal/push/PushServiceSocket.java | 78 +++++++++++-------- .../exceptions/PaymentsRegionException.java | 4 +- 8 files changed, 193 insertions(+), 47 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ImpossiblePhoneNumberException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonNormalizedPhoneNumberException.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt index 4904561ed7..49ba696e8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt @@ -1,9 +1,12 @@ package org.thoughtcrime.securesms.registration +import org.whispersystems.signalservice.api.push.exceptions.ImpossiblePhoneNumberException import org.whispersystems.signalservice.api.push.exceptions.LocalRateLimitException +import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.ServiceResponseProcessor import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse +import java.lang.IllegalStateException /** * Process responses from requesting an SMS or Phone code from the server. @@ -25,6 +28,32 @@ class RequestVerificationCodeResponseProcessor(response: ServiceResponse d.dismiss()) + .setNeutralButton(R.string.RegistrationActivity_contact_signal_support, (d, i) -> { + String subject = getString(R.string.RegistrationActivity_signal_android_phone_number_format); + String body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.RegistrationActivity_signal_android_phone_number_format, null, null); + + CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), subject, body); + d.dismiss(); + }) + .setPositiveButton(R.string.yes, (d, i) -> { + countryCode.setText(String.valueOf(phoneNumber.getCountryCode())); + number.setText(String.valueOf(phoneNumber.getNationalNumber())); + requestVerificationCode(mode); + d.dismiss(); + }) + .show(); + } catch (NumberParseException e) { + Log.w(TAG, "Failed to parse number!", e); + + Dialogs.showAlertDialog(requireContext(), + getString(R.string.RegistrationActivity_invalid_number), + String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), viewModel.getNumber().getFullFormattedNumber())); + } + } + private void handlePromptForNoPlayServices(@NonNull Context context) { new MaterialAlertDialogBuilder(context) .setTitle(R.string.RegistrationActivity_missing_google_play_services) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java index 87e7f89c01..ff93eb95db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Dialogs.java @@ -20,24 +20,25 @@ import android.content.Context; import androidx.appcompat.app.AlertDialog; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.thoughtcrime.securesms.R; public class Dialogs { public static void showAlertDialog(Context context, String title, String message) { - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - dialog.setTitle(title); - dialog.setMessage(message); - dialog.setIcon(R.drawable.ic_warning); - dialog.setPositiveButton(android.R.string.ok, null); - dialog.show(); + new MaterialAlertDialogBuilder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); } public static void showInfoDialog(Context context, String title, String message) { - AlertDialog.Builder dialog = new AlertDialog.Builder(context); - dialog.setTitle(title); - dialog.setMessage(message); - dialog.setIcon(R.drawable.ic_info_outline); - dialog.setPositiveButton(android.R.string.ok, null); - dialog.show(); + new MaterialAlertDialogBuilder(context) + .setTitle(title) + .setMessage(message) + .setIcon(R.drawable.ic_info_outline) + .setPositiveButton(android.R.string.ok, null) + .show(); } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1a5c8ad81..1a7f86f0ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1560,6 +1560,9 @@ Signal needs access to your contacts in order to connect with friends, exchange messages, and make secure calls You\'ve made too many attempts to register this number. Please try again later. Unable to connect to service. Please check network connection and try again. + Non-standard number format + The number you entered (%1$s) appears to be a non-standard format.\n\nDid you mean %2$s? + Signal Android - Phone Number Format Call requested You are now %d step away from submitting a debug log. diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ImpossiblePhoneNumberException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ImpossiblePhoneNumberException.java new file mode 100644 index 0000000000..ba6de25d03 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ImpossiblePhoneNumberException.java @@ -0,0 +1,10 @@ +package org.whispersystems.signalservice.api.push.exceptions; + +/** + * An exception indicating that the server believes the number provided is 'impossible', meaning it fails the most basic libphonenumber checks. + */ +public class ImpossiblePhoneNumberException extends NonSuccessfulResponseCodeException { + public ImpossiblePhoneNumberException() { + super(400); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonNormalizedPhoneNumberException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonNormalizedPhoneNumberException.java new file mode 100644 index 0000000000..9349309ce3 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonNormalizedPhoneNumberException.java @@ -0,0 +1,46 @@ +package org.whispersystems.signalservice.api.push.exceptions; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Response indicating we gave the server a non-normalized phone number. The expected normalized version of the number is provided. + */ +public class NonNormalizedPhoneNumberException extends NonSuccessfulResponseCodeException { + + private final String originalNumber; + private final String normalizedNumber; + + public static NonNormalizedPhoneNumberException forResponse(@NonNull String responseBody) throws MalformedResponseException { + JsonResponse response = JsonUtil.fromJsonResponse(responseBody, JsonResponse.class); + return new NonNormalizedPhoneNumberException(response.originalNumber, response.normalizedNumber); + } + + public NonNormalizedPhoneNumberException(String originalNumber, String normalizedNumber) { + super(400); + + this.originalNumber = originalNumber; + this.normalizedNumber = normalizedNumber; + } + + public String getOriginalNumber() { + return originalNumber; + } + + public String getNormalizedNumber() { + return normalizedNumber; + } + + private static class JsonResponse { + @JsonProperty + private String originalNumber; + + @JsonProperty + private String normalizedNumber; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 48e8aa6bb7..4226cc4d22 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -59,9 +59,11 @@ import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.ContactManifestMismatchException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException; +import org.whispersystems.signalservice.api.push.exceptions.ImpossiblePhoneNumberException; import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.NoContentException; +import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; @@ -141,7 +143,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.Future; @@ -304,14 +305,7 @@ public class PushServiceSocket { path += "&challenge=" + challenge.get(); } - makeServiceRequest(path, "GET", null, NO_HEADERS, new ResponseCodeHandler() { - @Override - public void handle(int responseCode) throws NonSuccessfulResponseCodeException { - if (responseCode == 402) { - throw new CaptchaRequiredException(); - } - } - }); + makeServiceRequest(path, "GET", null, NO_HEADERS, new VerificationCodeResponseHandler()); } public void requestVoiceVerificationCode(Locale locale, Optional captchaToken, Optional challenge) throws IOException { @@ -324,14 +318,7 @@ public class PushServiceSocket { path += "?challenge=" + challenge.get(); } - makeServiceRequest(path, "GET", null, headers, new ResponseCodeHandler() { - @Override - public void handle(int responseCode) throws NonSuccessfulResponseCodeException { - if (responseCode == 402) { - throw new CaptchaRequiredException(); - } - } - }); + makeServiceRequest(path, "GET", null, headers, new VerificationCodeResponseHandler()); } public UUID getOwnUuid() throws IOException { @@ -848,13 +835,10 @@ public class PushServiceSocket { } public void setUsername(String username) throws IOException { - makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, new ResponseCodeHandler() { - @Override - public void handle(int responseCode) throws NonSuccessfulResponseCodeException { - switch (responseCode) { - case 400: throw new UsernameMalformedException(); - case 409: throw new UsernameTakenException(); - } + makeServiceRequest(String.format(SET_USERNAME_PATH, username), "PUT", "", NO_HEADERS, (responseCode, body) -> { + switch (responseCode) { + case 400: throw new UsernameMalformedException(); + case 409: throw new UsernameTakenException(); } }, Optional.absent()); } @@ -932,7 +916,7 @@ public class PushServiceSocket { String.format(SUBSCRIPTION_RECEIPT_CREDENTIALS, subscriptionId), "POST", payload, - code -> { + (code, body) -> { if (code == 204) throw new NonSuccessfulResponseCodeException(204); }); @@ -1636,7 +1620,7 @@ public class PushServiceSocket { { Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey, doNotAddAuthenticationOrUnidentifiedAccessKey); - responseCodeHandler.handle(response.code()); + responseCodeHandler.handle(response.code(), response.body()); return validateServiceResponse(response); } @@ -1916,7 +1900,7 @@ public class PushServiceSocket { } } - responseCodeHandler.handle(response.code()); + responseCodeHandler.handle(response.code(), response.body()); switch (response.code()) { case 204: @@ -2231,12 +2215,12 @@ public class PushServiceSocket { } private interface ResponseCodeHandler { - void handle(int responseCode) throws NonSuccessfulResponseCodeException, PushNetworkException; + void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException; } private static class EmptyResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode) { } + public void handle(int responseCode, ResponseBody body) { } } public enum ClientSet { ContactDiscovery, KeyBackup } @@ -2254,20 +2238,20 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, CredentialResponse.class); } - private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = responseCode -> { + private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = (responseCode, body) -> { if (responseCode == 409) throw new GroupExistsException(); };; private static final ResponseCodeHandler GROUPS_V2_GET_LOGS_HANDLER = NO_HANDLER; - private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = responseCode -> { + private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = (responseCode, body) -> { switch (responseCode) { case 403: throw new NotInGroupException(); case 404: throw new GroupNotFoundException(); } }; - private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = responseCode -> { + private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = (responseCode, body) -> { if (responseCode == 400) throw new GroupPatchNotAcceptedException(); }; - private static final ResponseCodeHandler GROUPS_V2_GET_JOIN_INFO_HANDLER = responseCode -> { + private static final ResponseCodeHandler GROUPS_V2_GET_JOIN_INFO_HANDLER = (responseCode, body) -> { if (responseCode == 403) throw new ForbiddenException(); }; @@ -2404,6 +2388,34 @@ public class PushServiceSocket { makeServiceRequest(String.format(REPORT_SPAM, e164, serverGuid), "POST", ""); } + private static class VerificationCodeResponseHandler implements ResponseCodeHandler { + @Override + public void handle(int responseCode, ResponseBody responseBody) throws NonSuccessfulResponseCodeException, PushNetworkException { + switch (responseCode) { + case 400: + String body; + try { + body = responseBody != null ? responseBody.string() : ""; + } catch (IOException e) { + throw new PushNetworkException(e); + } + + if (body.isEmpty()) { + throw new ImpossiblePhoneNumberException(); + } else { + try { + throw NonNormalizedPhoneNumberException.forResponse(body); + } catch (MalformedResponseException e) { + Log.w(TAG, "Unable to parse 400 response! Assuming a generic 400."); + throw new ImpossiblePhoneNumberException(); + } + } + case 402: + throw new CaptchaRequiredException(); + } + } + } + public static final class GroupHistory { private final GroupChanges groupChanges; private final Optional contentRange; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java index 9ea9c75886..6ef914403a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java @@ -2,6 +2,8 @@ package org.whispersystems.signalservice.internal.push.exceptions; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import okhttp3.ResponseBody; + public final class PaymentsRegionException extends NonSuccessfulResponseCodeException { public PaymentsRegionException(int code) { super(code); @@ -10,7 +12,7 @@ public final class PaymentsRegionException extends NonSuccessfulResponseCodeExce /** * Promotes a 403 to this exception type. */ - public static void responseCodeHandler(int responseCode) throws PaymentsRegionException { + public static void responseCodeHandler(int responseCode, ResponseBody body) throws PaymentsRegionException { if (responseCode == 403) { throw new PaymentsRegionException(responseCode); }