Retire `RetryingApnsClient`
This commit is contained in:
parent
6f0faae4ce
commit
9e9333424f
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue