Skip to content

Commit 3a4a55c

Browse files
committed
Reject old-format Benin numbers, which are now undeliverable
1 parent f4a2438 commit 3a4a55c

File tree

7 files changed

+83
-20
lines changed

7 files changed

+83
-20
lines changed

service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
176176
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
177177
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
178+
import org.whispersystems.textsecuregcm.mappers.ObsoletePhoneNumberFormatExceptionMapper;
178179
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
179180
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
180181
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
@@ -1212,6 +1213,7 @@ private void registerExceptionMappers(Environment environment,
12121213
new ServerRejectedExceptionMapper(),
12131214
new ImpossiblePhoneNumberExceptionMapper(),
12141215
new NonNormalizedPhoneNumberExceptionMapper(),
1216+
new ObsoletePhoneNumberFormatExceptionMapper(),
12151217
new RegistrationServiceSenderExceptionMapper(),
12161218
new SubscriptionExceptionMapper(),
12171219
new JsonMappingExceptionMapper()

service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
9696
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
9797
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
98+
import org.whispersystems.textsecuregcm.util.ObsoletePhoneNumberFormatException;
9899
import org.whispersystems.textsecuregcm.util.Pair;
99100
import org.whispersystems.textsecuregcm.util.Util;
100101

@@ -173,7 +174,7 @@ public VerificationController(final RegistrationServiceClient registrationServic
173174
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed",
174175
schema = @Schema(implementation = Integer.class)))
175176
public VerificationSessionResponse createSession(@NotNull @Valid final CreateVerificationSessionRequest request)
176-
throws RateLimitExceededException {
177+
throws RateLimitExceededException, ObsoletePhoneNumberFormatException {
177178

178179
final Pair<String, PushNotification.TokenType> pushTokenAndType = validateAndExtractPushToken(
179180
request.getUpdateVerificationSessionRequest());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.whispersystems.textsecuregcm.mappers;
2+
3+
import io.micrometer.core.instrument.Metrics;
4+
import jakarta.ws.rs.core.Response;
5+
import jakarta.ws.rs.ext.ExceptionMapper;
6+
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
7+
import org.whispersystems.textsecuregcm.util.ObsoletePhoneNumberFormatException;
8+
9+
public class ObsoletePhoneNumberFormatExceptionMapper implements ExceptionMapper<ObsoletePhoneNumberFormatException> {
10+
11+
private static final String COUNTER_NAME = MetricsUtil.name(ObsoletePhoneNumberFormatExceptionMapper.class, "errors");
12+
13+
@Override
14+
public Response toResponse(final ObsoletePhoneNumberFormatException exception) {
15+
Metrics.counter(COUNTER_NAME, "regionCode", exception.getRegionCode()).increment();
16+
return Response.status(499).build();
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.whispersystems.textsecuregcm.util;
2+
3+
public class ObsoletePhoneNumberFormatException extends Exception {
4+
5+
private final String regionCode;
6+
7+
public ObsoletePhoneNumberFormatException(final String regionCode) {
8+
super("The provided format is obsolete in %s".formatted(regionCode));
9+
this.regionCode = regionCode;
10+
}
11+
12+
public String getRegionCode() {
13+
return regionCode;
14+
}
15+
}

service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.function.Function;
2828
import java.util.random.RandomGenerator;
2929
import java.util.stream.Collectors;
30-
3130
import org.apache.commons.lang3.StringUtils;
3231

3332
public class Util {
@@ -125,7 +124,7 @@ public static List<String> getAlternateForms(final String number) {
125124
try {
126125
final PhoneNumber phoneNumber = PHONE_NUMBER_UTIL.parse(number, null);
127126

128-
// Benin is changing phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024
127+
// Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX on November 30, 2024
129128
if ("BJ".equals(PHONE_NUMBER_UTIL.getRegionCodeForNumber(phoneNumber))) {
130129
final String nationalSignificantNumber = PHONE_NUMBER_UTIL.getNationalSignificantNumber(phoneNumber);
131130
final String alternateE164;
@@ -176,7 +175,7 @@ public static Optional<String> getCanonicalNumber(List<String> e164s) {
176175
throw new IllegalArgumentException("Numbers from different countries cannot be equivalent alternate forms");
177176
}
178177
if (regions.contains("BJ")) {
179-
// Benin is changing phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024
178+
// Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX on November 30, 2024
180179
// We prefer the longest form for long-term stability
181180
return e164s.stream().sorted(Comparator.comparingInt(String::length).reversed()).findFirst();
182181
}
@@ -217,7 +216,7 @@ public static boolean startsWithDecimal(final long number, final long prefix) {
217216
}
218217

219218
/**
220-
* Benin is changing phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024.
219+
* Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX on November 30, 2024
221220
*
222221
* @param phoneNumber the phone number to check.
223222
* @return whether the provided phone number is an old-format Benin phone number
@@ -235,11 +234,9 @@ public static boolean isOldFormatBeninPhoneNumber(final Phonenumber.PhoneNumber
235234
* @return the canonical phone number if applicable, otherwise the original phone number.
236235
*/
237236
public static Phonenumber.PhoneNumber canonicalizePhoneNumber(final Phonenumber.PhoneNumber phoneNumber)
238-
throws NumberParseException {
237+
throws NumberParseException, ObsoletePhoneNumberFormatException {
239238
if (isOldFormatBeninPhoneNumber(phoneNumber)) {
240-
// Benin changed phone number formats from +229 XXXXXXXX to +229 01XXXXXXXX starting on November 30, 2024.
241-
final String newFormatNumber = "+22901" + PHONE_NUMBER_UTIL.getNationalSignificantNumber(phoneNumber);
242-
return PhoneNumberUtil.getInstance().parse(newFormatNumber, null);
239+
throw new ObsoletePhoneNumberFormatException("bj");
243240
}
244241
return phoneNumber;
245242
}

service/src/test/java/org/whispersystems/textsecuregcm/controllers/VerificationControllerTest.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.whispersystems.textsecuregcm.limits.RateLimiters;
6767
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
6868
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
69+
import org.whispersystems.textsecuregcm.mappers.ObsoletePhoneNumberFormatExceptionMapper;
6970
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
7071
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
7172
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
@@ -117,6 +118,7 @@ class VerificationControllerTest {
117118
.addProvider(new RateLimitExceededExceptionMapper())
118119
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
119120
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
121+
.addProvider(new ObsoletePhoneNumberFormatExceptionMapper())
120122
.addProvider(new RegistrationServiceSenderExceptionMapper())
121123
.setMapper(SystemMapper.jsonMapper())
122124
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
@@ -220,7 +222,7 @@ void createBeninSessionSuccess(final String requestedNumber, final String expect
220222
when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any()))
221223
.thenReturn(
222224
CompletableFuture.completedFuture(
223-
new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null,
225+
new RegistrationServiceSession(SESSION_ID, requestedNumber, false, null, null, null,
224226
SESSION_EXPIRATION_SECONDS)));
225227
when(verificationSessionManager.insert(any(), any()))
226228
.thenReturn(CompletableFuture.completedFuture(null));
@@ -245,14 +247,36 @@ private static Stream<Arguments> createBeninSessionSuccess() {
245247
// libphonenumber 8.13.50 and on generate new-format numbers for Benin
246248
final String newFormatBeninE164 = PhoneNumberUtil.getInstance()
247249
.format(PhoneNumberUtil.getInstance().getExampleNumber("BJ"), PhoneNumberUtil.PhoneNumberFormat.E164);
248-
final String oldFormatBeninE164 = newFormatBeninE164.replaceFirst("01", "");
249250
return Stream.of(
250-
Arguments.of(oldFormatBeninE164, newFormatBeninE164),
251251
Arguments.of(newFormatBeninE164, newFormatBeninE164),
252252
Arguments.of(NUMBER, NUMBER)
253253
);
254254
}
255255

256+
@Test
257+
void createBeninSessionFailure() {
258+
// libphonenumber 8.13.50 and on generate new-format numbers for Benin
259+
final String newFormatBeninE164 = PhoneNumberUtil.getInstance()
260+
.format(PhoneNumberUtil.getInstance().getExampleNumber("BJ"), PhoneNumberUtil.PhoneNumberFormat.E164);
261+
final String oldFormatBeninE164 = newFormatBeninE164.replaceFirst("01", "");
262+
263+
when(registrationServiceClient.createRegistrationSession(any(), anyBoolean(), any()))
264+
.thenReturn(
265+
CompletableFuture.completedFuture(
266+
new RegistrationServiceSession(SESSION_ID, NUMBER, false, null, null, null,
267+
SESSION_EXPIRATION_SECONDS)));
268+
when(verificationSessionManager.insert(any(), any()))
269+
.thenReturn(CompletableFuture.completedFuture(null));
270+
271+
final Invocation.Builder request = resources.getJerseyTest()
272+
.target("/v1/verification/session")
273+
.request()
274+
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1");
275+
try (Response response = request.post(Entity.json(createSessionJson(oldFormatBeninE164, "token", "fcm")))) {
276+
assertEquals(499, response.getStatus());
277+
}
278+
}
279+
256280
@ParameterizedTest
257281
@MethodSource
258282
void createSessionSuccess(final String pushToken, final String pushTokenType,

service/src/test/java/org/whispersystems/textsecuregcm/util/UtilTest.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77

88
import static org.junit.jupiter.api.Assertions.assertEquals;
99
import static org.junit.jupiter.api.Assertions.assertFalse;
10+
import static org.junit.jupiter.api.Assertions.assertThrows;
1011
import static org.junit.jupiter.api.Assertions.assertTrue;
1112

1213
import com.google.i18n.phonenumbers.NumberParseException;
1314
import com.google.i18n.phonenumbers.PhoneNumberUtil;
15+
import com.google.i18n.phonenumbers.Phonenumber;
1416
import java.util.List;
1517
import java.util.Optional;
16-
import org.junit.jupiter.api.Test;
1718
import java.util.stream.Stream;
18-
import com.google.i18n.phonenumbers.Phonenumber;
19+
import javax.annotation.Nullable;
20+
import org.junit.jupiter.api.Test;
1921
import org.junit.jupiter.params.ParameterizedTest;
2022
import org.junit.jupiter.params.provider.Arguments;
2123
import org.junit.jupiter.params.provider.CsvSource;
@@ -93,19 +95,23 @@ private static Stream<Arguments> isOldFormatBeninPhoneNumber4() throws NumberPar
9395

9496
@ParameterizedTest
9597
@MethodSource
96-
void normalizeBeninPhoneNumber(final Phonenumber.PhoneNumber beninNumber, final Phonenumber.PhoneNumber expectedBeninNumber)
97-
throws NumberParseException {
98-
assertTrue(expectedBeninNumber.exactlySameAs(Util.canonicalizePhoneNumber(beninNumber)));
98+
void normalizeBeninPhoneNumber(final Phonenumber.PhoneNumber beninNumber, final Phonenumber.PhoneNumber expectedBeninNumber, @Nullable Class<? extends Throwable> exception)
99+
throws Exception {
100+
if (exception == null) {
101+
assertTrue(expectedBeninNumber.exactlySameAs(Util.canonicalizePhoneNumber(beninNumber)));
102+
} else {
103+
assertThrows(exception, () -> Util.canonicalizePhoneNumber(beninNumber));
104+
}
99105
}
100106

101107
private static Stream<Arguments> normalizeBeninPhoneNumber() throws NumberParseException {
102108
final Phonenumber.PhoneNumber oldFormatBeninPhoneNumber = PhoneNumberUtil.getInstance().parse(OLD_FORMAT_BENIN_E164_STRING, null);
103109
final Phonenumber.PhoneNumber newFormatBeninPhoneNumber = PhoneNumberUtil.getInstance().parse(NEW_FORMAT_BENIN_E164_STRING, null);
104110
final Phonenumber.PhoneNumber usPhoneNumber = PhoneNumberUtil.getInstance().getExampleNumber("US");
105111
return Stream.of(
106-
Arguments.of(newFormatBeninPhoneNumber, newFormatBeninPhoneNumber),
107-
Arguments.of(oldFormatBeninPhoneNumber, newFormatBeninPhoneNumber),
108-
Arguments.of(usPhoneNumber, usPhoneNumber)
112+
Arguments.of(newFormatBeninPhoneNumber, newFormatBeninPhoneNumber, null),
113+
Arguments.of(oldFormatBeninPhoneNumber, null, ObsoletePhoneNumberFormatException.class),
114+
Arguments.of(usPhoneNumber, usPhoneNumber, null)
109115
);
110116
}
111117
}

0 commit comments

Comments
 (0)