package org.whispersystems.textsecuregcm.push;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.nurkiewicz.asyncretry.AsyncRetryExecutor;
import com.nurkiewicz.asyncretry.RetryContext;
import com.nurkiewicz.asyncretry.RetryExecutor;
import com.nurkiewicz.asyncretry.function.RetryCallable;
import com.relayrides.pushy.apns.ApnsClient;
import com.relayrides.pushy.apns.ApnsClientBuilder;
import com.relayrides.pushy.apns.ApnsServerException;
import com.relayrides.pushy.apns.ClientNotConnectedException;
import com.relayrides.pushy.apns.DeliveryPriority;
import com.relayrides.pushy.apns.PushNotificationResponse;
import com.relayrides.pushy.apns.metrics.dropwizard.DropwizardApnsClientMetricsListener;
import com.relayrides.pushy.apns.util.SimpleApnsPushNotification;
import org.bouncycastle.openssl.PEMReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Constants;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static com.codahale.metrics.MetricRegistry.name;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
public class RetryingApnsClient {
private static final Logger logger = LoggerFactory.getLogger(RetryingApnsClient.class);
private final ApnsClient apnsClient;
private final RetryExecutor retryExecutor;
RetryingApnsClient(String apnCertificate, String apnKey, int retryCount)
throws IOException
{
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().setClientCredentials(initializeCertificate(apnCertificate),
initializePrivateKey(apnKey), null)
.setMetricsListener(metricsListener)
.build();
this.retryExecutor = initializeExecutor(retryCount);
}
@VisibleForTesting
public RetryingApnsClient(ApnsClient apnsClient, int retryCount) {
this.apnsClient = apnsClient;
this.retryExecutor = initializeExecutor(retryCount);
}
private static RetryExecutor initializeExecutor(int retryCount) {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
return new AsyncRetryExecutor(executorService).retryOn(ClientNotConnectedException.class)
.retryOn(InterruptedException.class)
.retryOn(ApnsServerException.class)
.withExponentialBackoff(100, 2.0)
.withUniformJitter()
.withMaxDelay(4000)
.withMaxRetries(retryCount);
}
ListenableFuture<ApnResult> send(final String apnId, final String topic, final String payload, final Date expiration) {
return this.retryExecutor.getFutureWithRetry(new RetryCallable<ListenableFuture<ApnResult>>() {
@Override
public ListenableFuture<ApnResult> call(RetryContext context) throws Exception {
SettableFuture<ApnResult> result = SettableFuture.create();
SimpleApnsPushNotification notification = new SimpleApnsPushNotification(apnId, topic, payload, expiration, DeliveryPriority.IMMEDIATE);
apnsClient.sendNotification(notification).addListener(new ResponseHandler(apnsClient, result));
return result;
}
});
}
void connect(boolean sandbox) {
apnsClient.connect(sandbox ? ApnsClient.DEVELOPMENT_APNS_HOST : ApnsClient.PRODUCTION_APNS_HOST).awaitUninterruptibly();
}
void disconnect() {
apnsClient.disconnect();
}
private static X509Certificate initializeCertificate(String pemCertificate) throws IOException {
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
return (X509Certificate) reader.readObject();
}
private static PrivateKey initializePrivateKey(String pemKey) throws IOException {
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemKey.getBytes())));
return ((KeyPair) reader.readObject()).getPrivate();
}
private static final class ResponseHandler implements GenericFutureListener<io.netty.util.concurrent.Future<PushNotificationResponse<SimpleApnsPushNotification>>> {
private final ApnsClient client;
private final SettableFuture<ApnResult> future;
private ResponseHandler(ApnsClient client, SettableFuture<ApnResult> future) {
this.client = client;
this.future = future;
}
@Override
public void operationComplete(io.netty.util.concurrent.Future<PushNotificationResponse<SimpleApnsPushNotification>> result) {
try {
PushNotificationResponse<SimpleApnsPushNotification> response = result.get();
if (response.isAccepted()) {
future.set(new ApnResult(ApnResult.Status.SUCCESS, null));
} else if ("Unregistered".equals(response.getRejectionReason())) {
future.set(new ApnResult(ApnResult.Status.NO_SUCH_USER, response.getRejectionReason()));
} else {
logger.warn("Got APN failure: " + response.getRejectionReason());
future.set(new ApnResult(ApnResult.Status.GENERIC_FAILURE, response.getRejectionReason()));
}
} catch (InterruptedException e) {
future.setException(e);
} catch (ExecutionException e) {
if (e.getCause() instanceof ClientNotConnectedException) setDisconnected(e.getCause());
else future.setException(e.getCause());
}
}
private void setDisconnected(final Throwable t) {
logger.warn("Client disconnected, waiting for reconnect...", t);
client.getReconnectionFuture().addListener(new GenericFutureListener<Future<Void>>() {
@Override
public void operationComplete(Future<Void> complete) {
logger.warn("Client reconnected...");
future.setException(t);
}
});
}
}
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;
}
}
}