From 02deea85e67ff3257c1499a89b7cf782a788b9d3 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 4 May 2017 09:41:35 -0700 Subject: [PATCH] Make apn unregister events work for voip push too // FREEBIE --- .../textsecuregcm/push/APNSender.java | 50 ++++-- .../tests/push/APNSenderTest.java | 153 ++++++++++++++++++ 2 files changed, 186 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java b/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java index 1e5f02d12..cc1324ad4 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java +++ b/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java @@ -130,23 +130,39 @@ public class APNSender implements Managed { Optional account = accountsManager.get(number); - if (account.isPresent()) { - Optional device = account.get().getDevice(deviceId); - - if (device.isPresent()) { - if (registrationId.equals(device.get().getApnId())) { - logger.info("APN Unregister APN ID matches!"); - if (device.get().getPushTimestamp() == 0 || - System.currentTimeMillis() > device.get().getPushTimestamp() + TimeUnit.SECONDS.toMillis(10)) - { - logger.info("APN Unregister timestamp matches!"); - device.get().setApnId(null); - device.get().setVoipApnId(null); - device.get().setFetchesMessages(false); - accountsManager.update(account.get()); - } - } - } + if (!account.isPresent()) { + logger.info("No account found: " + number); + return; } + + Optional device = account.get().getDevice(deviceId); + + if (!device.isPresent()) { + logger.info("No device found: " + number); + return; + } + + if (!registrationId.equals(device.get().getApnId()) && + !registrationId.equals(device.get().getVoipApnId())) + { + logger.info("Registration ID does not match: " + registrationId + ", " + device.get().getApnId() + ", " + device.get().getVoipApnId()); + return; + } + + logger.info("APN Unregister APN ID matches! " + number + ", " + deviceId); + + long tokenTimestamp = device.get().getPushTimestamp(); + + if (tokenTimestamp != 0 && System.currentTimeMillis() < tokenTimestamp + TimeUnit.SECONDS.toMillis(10)) + { + logger.info("APN Unregister push timestamp is more recent: " + tokenTimestamp + ", " + number); + return; + } + + logger.info("APN Unregister timestamp matches!"); + device.get().setApnId(null); + device.get().setVoipApnId(null); + device.get().setFetchesMessages(false); + accountsManager.update(account.get()); } } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java index 461081d09..f548314cb 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java @@ -21,6 +21,7 @@ import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService; import java.util.Date; +import java.util.concurrent.TimeUnit; import io.netty.util.concurrent.DefaultEventExecutor; import io.netty.util.concurrent.DefaultPromise; @@ -134,6 +135,9 @@ public class APNSenderTest { ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, "message", true, 30); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); + when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID); + when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11)); + ListenableFuture sendFuture = apnSender.sendMessage(message); ApnResult apnResult = sendFuture.get(); @@ -151,13 +155,162 @@ public class APNSenderTest { verifyNoMoreInteractions(apnsClient); verify(accountsManager, times(1)).get(eq(DESTINATION_NUMBER)); + verify(destinationAccount, times(1)).getDevice(1); verify(destinationDevice, times(1)).getApnId(); + verify(destinationDevice, times(1)).getPushTimestamp(); verify(destinationDevice, times(1)).setApnId(eq((String)null)); + verify(destinationDevice, times(1)).setVoipApnId(eq((String)null)); + verify(destinationDevice, times(1)).setFetchesMessages(eq(false)); verify(accountsManager, times(1)).update(eq(destinationAccount)); verifyNoMoreInteractions(accountsManager); } + @Test + public void testVoipUnregisteredUser() throws Exception { + ApnsClient apnsClient = mock(ApnsClient.class); + + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(false); + when(response.getRejectionReason()).thenReturn("Unregistered"); + + DefaultPromise> result = new DefaultPromise<>(executor); + result.setSuccess(response); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenReturn(result); + + RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient, 10); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, "message", true, 30); + APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); + + when(destinationDevice.getApnId()).thenReturn("baz"); + when(destinationDevice.getVoipApnId()).thenReturn(DESTINATION_APN_ID); + when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11)); + + ListenableFuture sendFuture = apnSender.sendMessage(message); + ApnResult apnResult = sendFuture.get(); + + Thread.sleep(1000); // =( + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient, times(1)).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(30)); + assertThat(notification.getValue().getPayload()).isEqualTo("message"); + assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); + + assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); + + verifyNoMoreInteractions(apnsClient); + verify(accountsManager, times(1)).get(eq(DESTINATION_NUMBER)); + verify(destinationAccount, times(1)).getDevice(1); + verify(destinationDevice, times(1)).getApnId(); + verify(destinationDevice, times(1)).getVoipApnId(); + verify(destinationDevice, times(1)).getPushTimestamp(); + verify(destinationDevice, times(1)).setApnId(eq((String)null)); + verify(destinationDevice, times(1)).setVoipApnId(eq((String)null)); + verify(destinationDevice, times(1)).setFetchesMessages(eq(false)); + verify(accountsManager, times(1)).update(eq(destinationAccount)); + + verifyNoMoreInteractions(accountsManager); + } + + @Test + public void testRecentUnregisteredUser() throws Exception { + ApnsClient apnsClient = mock(ApnsClient.class); + + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(false); + when(response.getRejectionReason()).thenReturn("Unregistered"); + + DefaultPromise> result = new DefaultPromise<>(executor); + result.setSuccess(response); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenReturn(result); + + RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient, 10); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, "message", true, 30); + APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); + + when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID); + when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis()); + + ListenableFuture sendFuture = apnSender.sendMessage(message); + ApnResult apnResult = sendFuture.get(); + + Thread.sleep(1000); // =( + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient, times(1)).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(30)); + assertThat(notification.getValue().getPayload()).isEqualTo("message"); + assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); + + assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); + + verifyNoMoreInteractions(apnsClient); + verify(accountsManager, times(1)).get(eq(DESTINATION_NUMBER)); + verify(destinationAccount, times(1)).getDevice(1); + verify(destinationDevice, times(1)).getApnId(); + verify(destinationDevice, times(1)).getPushTimestamp(); + + verifyNoMoreInteractions(destinationDevice); + verifyNoMoreInteractions(destinationAccount); + verifyNoMoreInteractions(accountsManager); + } + + @Test + public void testUnregisteredUserOldApnId() throws Exception { + ApnsClient apnsClient = mock(ApnsClient.class); + + PushNotificationResponse response = mock(PushNotificationResponse.class); + when(response.isAccepted()).thenReturn(false); + when(response.getRejectionReason()).thenReturn("Unregistered"); + + DefaultPromise> result = new DefaultPromise<>(executor); + result.setSuccess(response); + + when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) + .thenReturn(result); + + RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient, 10); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, "message", true, 30); + APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); + + when(destinationDevice.getApnId()).thenReturn("baz"); + when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(12)); + + ListenableFuture sendFuture = apnSender.sendMessage(message); + ApnResult apnResult = sendFuture.get(); + + Thread.sleep(1000); // =( + + ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); + verify(apnsClient, times(1)).sendNotification(notification.capture()); + + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(30)); + assertThat(notification.getValue().getPayload()).isEqualTo("message"); + assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); + + assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); + + verifyNoMoreInteractions(apnsClient); + verify(accountsManager, times(1)).get(eq(DESTINATION_NUMBER)); + verify(destinationAccount, times(1)).getDevice(1); + verify(destinationDevice, times(2)).getApnId(); + verify(destinationDevice, times(2)).getVoipApnId(); + + verifyNoMoreInteractions(destinationDevice); + verifyNoMoreInteractions(destinationAccount); + verifyNoMoreInteractions(accountsManager); + } + @Test public void testGenericFailure() throws Exception { ApnsClient apnsClient = mock(ApnsClient.class);