Reject old-format Benin numbers, which are now undeliverable
This commit is contained in:
parent
f4a243861c
commit
3a4a55c245
|
@ -175,6 +175,7 @@ import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMa
|
||||||
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.ObsoletePhoneNumberFormatExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
||||||
|
@ -1212,6 +1213,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new ServerRejectedExceptionMapper(),
|
new ServerRejectedExceptionMapper(),
|
||||||
new ImpossiblePhoneNumberExceptionMapper(),
|
new ImpossiblePhoneNumberExceptionMapper(),
|
||||||
new NonNormalizedPhoneNumberExceptionMapper(),
|
new NonNormalizedPhoneNumberExceptionMapper(),
|
||||||
|
new ObsoletePhoneNumberFormatExceptionMapper(),
|
||||||
new RegistrationServiceSenderExceptionMapper(),
|
new RegistrationServiceSenderExceptionMapper(),
|
||||||
new SubscriptionExceptionMapper(),
|
new SubscriptionExceptionMapper(),
|
||||||
new JsonMappingExceptionMapper()
|
new JsonMappingExceptionMapper()
|
||||||
|
|
|
@ -95,6 +95,7 @@ import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
||||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ObsoletePhoneNumberFormatException;
|
||||||
import org.whispersystems.textsecuregcm.util.Pair;
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
@ -173,7 +174,7 @@ public class VerificationController {
|
||||||
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed",
|
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed",
|
||||||
schema = @Schema(implementation = Integer.class)))
|
schema = @Schema(implementation = Integer.class)))
|
||||||
public VerificationSessionResponse createSession(@NotNull @Valid final CreateVerificationSessionRequest request)
|
public VerificationSessionResponse createSession(@NotNull @Valid final CreateVerificationSessionRequest request)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException, ObsoletePhoneNumberFormatException {
|
||||||
|
|
||||||
final Pair<String, PushNotification.TokenType> pushTokenAndType = validateAndExtractPushToken(
|
final Pair<String, PushNotification.TokenType> pushTokenAndType = validateAndExtractPushToken(
|
||||||
request.getUpdateVerificationSessionRequest());
|
request.getUpdateVerificationSessionRequest());
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.whispersystems.textsecuregcm.mappers;
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ObsoletePhoneNumberFormatException;
|
||||||
|
|
||||||
|
public class ObsoletePhoneNumberFormatExceptionMapper implements ExceptionMapper<ObsoletePhoneNumberFormatException> {
|
||||||
|
|
||||||
|
private static final String COUNTER_NAME = MetricsUtil.name(ObsoletePhoneNumberFormatExceptionMapper.class, "errors");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(final ObsoletePhoneNumberFormatException exception) {
|
||||||
|
Metrics.counter(COUNTER_NAME, "regionCode", exception.getRegionCode()).increment();
|
||||||
|
return Response.status(499).build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
|
public class ObsoletePhoneNumberFormatException extends Exception {
|
||||||
|
|
||||||
|
private final String regionCode;
|
||||||
|
|
||||||
|
public ObsoletePhoneNumberFormatException(final String regionCode) {
|
||||||
|
super("The provided format is obsolete in %s".formatted(regionCode));
|
||||||
|
this.regionCode = regionCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRegionCode() {
|
||||||
|
return regionCode;
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.random.RandomGenerator;
|
import java.util.random.RandomGenerator;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
public class Util {
|
public class Util {
|
||||||
|
@ -125,7 +124,7 @@ public class Util {
|
||||||
try {
|
try {
|
||||||
final PhoneNumber phoneNumber = PHONE_NUMBER_UTIL.parse(number, null);
|
final PhoneNumber phoneNumber = PHONE_NUMBER_UTIL.parse(number, null);
|
||||||
|
|
||||||
// Benin is changing phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024
|
// Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX on November 30, 2024
|
||||||
if ("BJ".equals(PHONE_NUMBER_UTIL.getRegionCodeForNumber(phoneNumber))) {
|
if ("BJ".equals(PHONE_NUMBER_UTIL.getRegionCodeForNumber(phoneNumber))) {
|
||||||
final String nationalSignificantNumber = PHONE_NUMBER_UTIL.getNationalSignificantNumber(phoneNumber);
|
final String nationalSignificantNumber = PHONE_NUMBER_UTIL.getNationalSignificantNumber(phoneNumber);
|
||||||
final String alternateE164;
|
final String alternateE164;
|
||||||
|
@ -176,7 +175,7 @@ public class Util {
|
||||||
throw new IllegalArgumentException("Numbers from different countries cannot be equivalent alternate forms");
|
throw new IllegalArgumentException("Numbers from different countries cannot be equivalent alternate forms");
|
||||||
}
|
}
|
||||||
if (regions.contains("BJ")) {
|
if (regions.contains("BJ")) {
|
||||||
// Benin is changing phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024
|
// Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX on November 30, 2024
|
||||||
// We prefer the longest form for long-term stability
|
// We prefer the longest form for long-term stability
|
||||||
return e164s.stream().sorted(Comparator.comparingInt(String::length).reversed()).findFirst();
|
return e164s.stream().sorted(Comparator.comparingInt(String::length).reversed()).findFirst();
|
||||||
}
|
}
|
||||||
|
@ -217,7 +216,7 @@ public class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benin is changing phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024.
|
* Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX on November 30, 2024
|
||||||
*
|
*
|
||||||
* @param phoneNumber the phone number to check.
|
* @param phoneNumber the phone number to check.
|
||||||
* @return whether the provided phone number is an old-format Benin phone number
|
* @return whether the provided phone number is an old-format Benin phone number
|
||||||
|
@ -235,11 +234,9 @@ public class Util {
|
||||||
* @return the canonical phone number if applicable, otherwise the original phone number.
|
* @return the canonical phone number if applicable, otherwise the original phone number.
|
||||||
*/
|
*/
|
||||||
public static Phonenumber.PhoneNumber canonicalizePhoneNumber(final Phonenumber.PhoneNumber phoneNumber)
|
public static Phonenumber.PhoneNumber canonicalizePhoneNumber(final Phonenumber.PhoneNumber phoneNumber)
|
||||||
throws NumberParseException {
|
throws NumberParseException, ObsoletePhoneNumberFormatException {
|
||||||
if (isOldFormatBeninPhoneNumber(phoneNumber)) {
|
if (isOldFormatBeninPhoneNumber(phoneNumber)) {
|
||||||
// Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024.
|
throw new ObsoletePhoneNumberFormatException("bj");
|
||||||
final String newFormatNumber = "+22901" + PHONE_NUMBER_UTIL.getNationalSignificantNumber(phoneNumber);
|
|
||||||
return PhoneNumberUtil.getInstance().parse(newFormatNumber, null);
|
|
||||||
}
|
}
|
||||||
return phoneNumber;
|
return phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.ObsoletePhoneNumberFormatExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
|
@ -117,6 +118,7 @@ class VerificationControllerTest {
|
||||||
.addProvider(new RateLimitExceededExceptionMapper())
|
.addProvider(new RateLimitExceededExceptionMapper())
|
||||||
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
|
||||||
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
|
||||||
|
.addProvider(new ObsoletePhoneNumberFormatExceptionMapper())
|
||||||
.addProvider(new RegistrationServiceSenderExceptionMapper())
|
.addProvider(new RegistrationServiceSenderExceptionMapper())
|
||||||
.setMapper(SystemMapper.jsonMapper())
|
.setMapper(SystemMapper.jsonMapper())
|
||||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||||
|
@ -220,7 +222,7 @@ class VerificationControllerTest {
|
||||||
when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any()))
|
when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any()))
|
||||||
.thenReturn(
|
.thenReturn(
|
||||||
CompletableFuture.completedFuture(
|
CompletableFuture.completedFuture(
|
||||||
new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null,
|
new RegistrationServiceSession(SESSION_ID, requestedNumber, false, null, null, null,
|
||||||
SESSION_EXPIRATION_SECONDS)));
|
SESSION_EXPIRATION_SECONDS)));
|
||||||
when(verificationSessionManager.insert(any(), any()))
|
when(verificationSessionManager.insert(any(), any()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(null));
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
@ -245,14 +247,36 @@ class VerificationControllerTest {
|
||||||
// libphonenumber 8.13.50 and on generate new-format numbers for Benin
|
// libphonenumber 8.13.50 and on generate new-format numbers for Benin
|
||||||
final String newFormatBeninE164 = PhoneNumberUtil.getInstance()
|
final String newFormatBeninE164 = PhoneNumberUtil.getInstance()
|
||||||
.format(PhoneNumberUtil.getInstance().getExampleNumber("BJ"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
.format(PhoneNumberUtil.getInstance().getExampleNumber("BJ"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
final String oldFormatBeninE164 = newFormatBeninE164.replaceFirst("01", "");
|
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(oldFormatBeninE164, newFormatBeninE164),
|
|
||||||
Arguments.of(newFormatBeninE164, newFormatBeninE164),
|
Arguments.of(newFormatBeninE164, newFormatBeninE164),
|
||||||
Arguments.of(NUMBER, NUMBER)
|
Arguments.of(NUMBER, NUMBER)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createBeninSessionFailure() {
|
||||||
|
// libphonenumber 8.13.50 and on generate new-format numbers for Benin
|
||||||
|
final String newFormatBeninE164 = PhoneNumberUtil.getInstance()
|
||||||
|
.format(PhoneNumberUtil.getInstance().getExampleNumber("BJ"), PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||||
|
final String oldFormatBeninE164 = newFormatBeninE164.replaceFirst("01", "");
|
||||||
|
|
||||||
|
when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any()))
|
||||||
|
.thenReturn(
|
||||||
|
CompletableFuture.completedFuture(
|
||||||
|
new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null,
|
||||||
|
SESSION_EXPIRATION_SECONDS)));
|
||||||
|
when(verificationSessionManager.insert(any(), any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(null));
|
||||||
|
|
||||||
|
final Invocation.Builder request = resources.getJerseyTest()
|
||||||
|
.target("/v1/verification/session")
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1");
|
||||||
|
try (Response response = request.post(Entity.json(createSessionJson(oldFormatBeninE164, "token", "fcm")))) {
|
||||||
|
assertEquals(499, response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void createSessionSuccess(final String pushToken, final String pushTokenType,
|
void createSessionSuccess(final String pushToken, final String pushTokenType,
|
||||||
|
|
|
@ -7,15 +7,17 @@ package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.google.i18n.phonenumbers.NumberParseException;
|
import com.google.i18n.phonenumbers.NumberParseException;
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import com.google.i18n.phonenumbers.Phonenumber;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import com.google.i18n.phonenumbers.Phonenumber;
|
import javax.annotation.Nullable;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
@ -93,9 +95,13 @@ class UtilTest {
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void normalizeBeninPhoneNumber(final Phonenumber.PhoneNumber beninNumber, final Phonenumber.PhoneNumber expectedBeninNumber)
|
void normalizeBeninPhoneNumber(final Phonenumber.PhoneNumber beninNumber, final Phonenumber.PhoneNumber expectedBeninNumber, @Nullable Class<? extends Throwable> exception)
|
||||||
throws NumberParseException {
|
throws Exception {
|
||||||
assertTrue(expectedBeninNumber.exactlySameAs(Util.canonicalizePhoneNumber(beninNumber)));
|
if (exception == null) {
|
||||||
|
assertTrue(expectedBeninNumber.exactlySameAs(Util.canonicalizePhoneNumber(beninNumber)));
|
||||||
|
} else {
|
||||||
|
assertThrows(exception, () -> Util.canonicalizePhoneNumber(beninNumber));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<Arguments> normalizeBeninPhoneNumber() throws NumberParseException {
|
private static Stream<Arguments> normalizeBeninPhoneNumber() throws NumberParseException {
|
||||||
|
@ -103,9 +109,9 @@ class UtilTest {
|
||||||
final Phonenumber.PhoneNumber newFormatBeninPhoneNumber = PhoneNumberUtil.getInstance().parse(NEW_FORMAT_BENIN_E164_STRING, null);
|
final Phonenumber.PhoneNumber newFormatBeninPhoneNumber = PhoneNumberUtil.getInstance().parse(NEW_FORMAT_BENIN_E164_STRING, null);
|
||||||
final Phonenumber.PhoneNumber usPhoneNumber = PhoneNumberUtil.getInstance().getExampleNumber("US");
|
final Phonenumber.PhoneNumber usPhoneNumber = PhoneNumberUtil.getInstance().getExampleNumber("US");
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(newFormatBeninPhoneNumber, newFormatBeninPhoneNumber),
|
Arguments.of(newFormatBeninPhoneNumber, newFormatBeninPhoneNumber, null),
|
||||||
Arguments.of(oldFormatBeninPhoneNumber, newFormatBeninPhoneNumber),
|
Arguments.of(oldFormatBeninPhoneNumber, null, ObsoletePhoneNumberFormatException.class),
|
||||||
Arguments.of(usPhoneNumber, usPhoneNumber)
|
Arguments.of(usPhoneNumber, usPhoneNumber, null)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue