Retire `RetryingApnsClient`

This commit is contained in:
Jon Chambers 2022-08-03 11:27:02 -04:00 committed by Jon Chambers
parent 6f0faae4ce
commit 9e9333424f
3 changed files with 49 additions and 199 deletions

View File

@ -4,27 +4,28 @@
*/
package org.whispersystems.textsecuregcm.push;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.ApnsClientBuilder;
import com.eatthepath.pushy.apns.DeliveryPriority;
import com.eatthepath.pushy.apns.PushType;
import com.eatthepath.pushy.apns.auth.ApnsSigningKey;
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import io.dropwizard.lifecycle.Managed;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nullable;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.push.RetryingApnsClient.ApnResult;
public class APNSender implements Managed, PushNotificationSender {
private final ExecutorService executor;
private final String bundleId;
private final boolean sandbox;
private final RetryingApnsClient apnsClient;
private final ExecutorService executor;
private final String bundleId;
private final ApnsClient apnsClient;
@VisibleForTesting
static final String APN_VOIP_NOTIFICATION_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}";
@ -41,24 +42,26 @@ public class APNSender implements Managed, PushNotificationSender {
@VisibleForTesting
static final Instant MAX_EXPIRATION = Instant.ofEpochMilli(Integer.MAX_VALUE * 1000L);
private static final String APNS_CA_FILENAME = "apns-certificates.pem";
public APNSender(ExecutorService executor, ApnConfiguration configuration)
throws IOException, NoSuchAlgorithmException, InvalidKeyException
{
this.executor = executor;
this.bundleId = configuration.getBundleId();
this.sandbox = configuration.isSandboxEnabled();
this.apnsClient = new RetryingApnsClient(configuration.getSigningKey(),
configuration.getTeamId(),
configuration.getKeyId(),
sandbox);
this.executor = executor;
this.bundleId = configuration.getBundleId();
this.apnsClient = new ApnsClientBuilder().setSigningKey(
ApnsSigningKey.loadFromInputStream(new ByteArrayInputStream(configuration.getSigningKey().getBytes()),
configuration.getTeamId(), configuration.getKeyId()))
.setTrustedServerCertificateChain(getClass().getResourceAsStream(APNS_CA_FILENAME))
.setApnsServer(configuration.isSandboxEnabled() ? ApnsClientBuilder.DEVELOPMENT_APNS_HOST : ApnsClientBuilder.PRODUCTION_APNS_HOST)
.build();
}
@VisibleForTesting
public APNSender(ExecutorService executor, RetryingApnsClient apnsClient, String bundleId, boolean sandbox) {
this.executor = executor;
this.apnsClient = apnsClient;
this.sandbox = sandbox;
this.bundleId = bundleId;
public APNSender(ExecutorService executor, ApnsClient apnsClient, String bundleId) {
this.executor = executor;
this.apnsClient = apnsClient;
this.bundleId = bundleId;
}
@Override
@ -81,37 +84,30 @@ public class APNSender implements Managed, PushNotificationSender {
(notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && !isVoip)
? "incoming-message" : null;
final ListenableFuture<ApnResult> sendFuture = apnsClient.send(notification.deviceToken(),
return apnsClient.sendNotification(new SimpleApnsPushNotification(notification.deviceToken(),
topic,
payload,
MAX_EXPIRATION,
isVoip,
collapseId);
DeliveryPriority.IMMEDIATE,
isVoip ? PushType.VOIP : PushType.ALERT,
collapseId))
.thenApplyAsync(response -> {
final boolean accepted;
final String rejectionReason;
final boolean unregistered;
final CompletableFuture<SendPushNotificationResult> completableSendFuture = new CompletableFuture<>();
if (response.isAccepted()) {
accepted = true;
rejectionReason = null;
unregistered = false;
} else {
accepted = false;
rejectionReason = response.getRejectionReason().orElse("unknown");
unregistered = ("Unregistered".equals(rejectionReason) || "BadDeviceToken".equals(rejectionReason));
}
Futures.addCallback(sendFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable ApnResult result) {
if (result == null) {
// This should never happen
completableSendFuture.completeExceptionally(new NullPointerException("apnResult was null"));
} else {
completableSendFuture.complete(switch (result.getStatus()) {
case SUCCESS -> new SendPushNotificationResult(true, null, false);
case NO_SUCH_USER -> new SendPushNotificationResult(false, result.getReason(), true);
case GENERIC_FAILURE -> new SendPushNotificationResult(false, result.getReason(), false);
});
}
}
@Override
public void onFailure(@Nullable Throwable t) {
completableSendFuture.completeExceptionally(t);
}
}, executor);
return completableSendFuture;
return new SendPushNotificationResult(accepted, rejectionReason, unregistered);
}, executor);
}
@Override
@ -120,6 +116,6 @@ public class APNSender implements Managed, PushNotificationSender {
@Override
public void stop() {
this.apnsClient.disconnect();
this.apnsClient.close().join();
}
}

View File

@ -1,141 +0,0 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.ApnsClientBuilder;
import com.eatthepath.pushy.apns.DeliveryPriority;
import com.eatthepath.pushy.apns.PushNotificationResponse;
import com.eatthepath.pushy.apns.PushType;
import com.eatthepath.pushy.apns.auth.ApnsSigningKey;
import com.eatthepath.pushy.apns.metrics.dropwizard.DropwizardApnsClientMetricsListener;
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import io.micrometer.core.instrument.Metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Map;
import java.util.function.BiConsumer;
import static com.codahale.metrics.MetricRegistry.name;
public class RetryingApnsClient {
private static final String APNS_CA_FILENAME = "apns-certificates.pem";
private static final Logger logger = LoggerFactory.getLogger(RetryingApnsClient.class);
private static final String RESPONSE_COUNTER_NAME = name(RetryingApnsClient.class, "response");
private static final String ACCEPTED_TAG_NAME = "accepted";
private static final String REJECTION_REASON_TAG_NAME = "rejectionReason";
private final ApnsClient apnsClient;
RetryingApnsClient(String apnSigningKey, String teamId, String keyId, boolean sandbox)
throws IOException, InvalidKeyException, NoSuchAlgorithmException
{
MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
DropwizardApnsClientMetricsListener metricsListener = new DropwizardApnsClientMetricsListener();
for (Map.Entry<String, Metric> entry : metricsListener.getMetrics().entrySet()) {
metricRegistry.register(name(getClass(), entry.getKey()), entry.getValue());
}
this.apnsClient = new ApnsClientBuilder().setSigningKey(ApnsSigningKey.loadFromInputStream(new ByteArrayInputStream(apnSigningKey.getBytes()), teamId, keyId))
.setMetricsListener(metricsListener)
.setTrustedServerCertificateChain(getClass().getResourceAsStream(APNS_CA_FILENAME))
.setApnsServer(sandbox ? ApnsClientBuilder.DEVELOPMENT_APNS_HOST : ApnsClientBuilder.PRODUCTION_APNS_HOST)
.build();
}
@VisibleForTesting
public RetryingApnsClient(ApnsClient apnsClient) {
this.apnsClient = apnsClient;
}
ListenableFuture<ApnResult> send(final String apnId, final String topic, final String payload, final Instant expiration, final boolean isVoip, final String collapseId) {
SettableFuture<ApnResult> result = SettableFuture.create();
SimpleApnsPushNotification notification = new SimpleApnsPushNotification(apnId, topic, payload, expiration, DeliveryPriority.IMMEDIATE, isVoip ? PushType.VOIP : PushType.ALERT, collapseId);
apnsClient.sendNotification(notification).whenComplete(new ResponseHandler(result));
return result;
}
void disconnect() {
apnsClient.close();
}
private static final class ResponseHandler implements BiConsumer<PushNotificationResponse<SimpleApnsPushNotification>, Throwable> {
private final SettableFuture<ApnResult> future;
private ResponseHandler(SettableFuture<ApnResult> future) {
this.future = future;
}
@Override
public void accept(final PushNotificationResponse<SimpleApnsPushNotification> response, final Throwable cause) {
if (response != null) {
if (response.isAccepted()) {
future.set(new ApnResult(ApnResult.Status.SUCCESS, null));
Metrics.counter(RESPONSE_COUNTER_NAME, ACCEPTED_TAG_NAME, "true").increment();
} else {
final String rejectionReason = response.getRejectionReason().orElse(null);
Metrics.counter(RESPONSE_COUNTER_NAME,
ACCEPTED_TAG_NAME, "false",
REJECTION_REASON_TAG_NAME, rejectionReason).increment();
if ("Unregistered".equals(rejectionReason) || "BadDeviceToken".equals(rejectionReason)) {
future.set(new ApnResult(ApnResult.Status.NO_SUCH_USER, rejectionReason));
} else {
logger.warn("Got APN failure: {}", rejectionReason);
future.set(new ApnResult(ApnResult.Status.GENERIC_FAILURE, rejectionReason));
}
}
} else {
logger.warn("Execution exception", cause);
future.setException(cause);
}
}
}
public static class ApnResult {
public enum Status {
SUCCESS, NO_SUCH_USER, GENERIC_FAILURE
}
private final Status status;
private final String reason;
ApnResult(Status status, String reason) {
this.status = status;
this.reason = reason;
}
public Status getStatus() {
return status;
}
public String getReason() {
return reason;
}
}
}

View File

@ -59,9 +59,8 @@ class APNSenderTest {
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
@ -89,9 +88,8 @@ class APNSenderTest {
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
@ -121,9 +119,8 @@ class APNSenderTest {
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11));
@ -154,9 +151,8 @@ class APNSenderTest {
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response));
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
@ -181,9 +177,8 @@ class APNSenderTest {
when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
.thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), new IOException("lost connection")));
RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient);
PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), retryingApnsClient, "foo", false);
APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join())
.isInstanceOf(CompletionException.class)