From f8063f8faf2d38ad3d3bc13ce9a5cd7e60ed556b Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 1 Dec 2014 13:27:06 -0800 Subject: [PATCH] Add feedback handler. // FREEBIE --- .../textsecuregcm/WhisperServerService.java | 4 + .../entities/UnregisteredEvent.java | 33 +++++++ .../entities/UnregisteredEventList.java | 17 ++++ .../textsecuregcm/push/FeedbackHandler.java | 99 +++++++++++++++++++ .../textsecuregcm/push/PushServiceClient.java | 55 +++++++++-- 5 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/push/FeedbackHandler.java diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index e37ca9551..fd2365de0 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -54,6 +54,7 @@ import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory; import org.whispersystems.textsecuregcm.providers.RedisClientFactory; import org.whispersystems.textsecuregcm.providers.RedisHealthCheck; import org.whispersystems.textsecuregcm.providers.TimeProvider; +import org.whispersystems.textsecuregcm.push.FeedbackHandler; import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.push.PushServiceClient; import org.whispersystems.textsecuregcm.push.WebsocketSender; @@ -158,8 +159,11 @@ public class WhisperServerService extends Application authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey(); + environment.lifecycle().manage(feedbackHandler); + AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner); KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager); diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java b/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java new file mode 100644 index 000000000..af3d3af50 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEvent.java @@ -0,0 +1,33 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.Min; + +public class UnregisteredEvent { + + @JsonProperty + @NotEmpty + private String registrationId; + + @JsonProperty + @NotEmpty + private String number; + + @JsonProperty + @Min(1) + private int deviceId; + + public String getRegistrationId() { + return registrationId; + } + + public String getNumber() { + return number; + } + + public int getDeviceId() { + return deviceId; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java new file mode 100644 index 000000000..87b23ec4e --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/UnregisteredEventList.java @@ -0,0 +1,17 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.LinkedList; +import java.util.List; + +public class UnregisteredEventList { + + @JsonProperty + private List devices; + + public List getDevices() { + if (devices == null) return new LinkedList<>(); + else return devices; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/push/FeedbackHandler.java b/src/main/java/org/whispersystems/textsecuregcm/push/FeedbackHandler.java new file mode 100644 index 000000000..cd1a41d0a --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/push/FeedbackHandler.java @@ -0,0 +1,99 @@ +package org.whispersystems.textsecuregcm.push; + +import com.google.common.base.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.entities.UnregisteredEvent; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.dropwizard.lifecycle.Managed; + +public class FeedbackHandler implements Managed, Runnable { + + private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class); + + private final PushServiceClient client; + private final AccountsManager accountsManager; + + private ScheduledExecutorService executor; + + public FeedbackHandler(PushServiceClient client, AccountsManager accountsManager) { + this.client = client; + this.accountsManager = accountsManager; + } + + @Override + public void start() throws Exception { + this.executor = Executors.newSingleThreadScheduledExecutor(); + this.executor.scheduleAtFixedRate(this, 0, 10, TimeUnit.MINUTES); + } + + @Override + public void stop() throws Exception { + if (this.executor != null) { + this.executor.shutdown(); + } + } + + @Override + public void run() { + try { + List gcmFeedback = client.getGcmFeedback(); + List apnFeedback = client.getApnFeedback(); + + for (UnregisteredEvent gcmEvent : gcmFeedback) { + handleGcmUnregistered(gcmEvent); + } + + for (UnregisteredEvent apnEvent : apnFeedback) { + handleApnUnregistered(apnEvent); + } + } catch (IOException e) { + logger.warn("Error retrieving feedback: ", e); + } + } + + private void handleGcmUnregistered(UnregisteredEvent event) { + logger.warn("Got GCM Unregistered: " + event.getNumber() + "," + event.getDeviceId()); + + Optional account = accountsManager.get(event.getNumber()); + + if (account.isPresent()) { + Optional device = account.get().getDevice(event.getDeviceId()); + + if (device.isPresent()) { + if (event.getRegistrationId().equals(device.get().getGcmId())) { + logger.warn("GCM Unregister GCM ID matches!"); + device.get().setGcmId(null); + accountsManager.update(account.get()); + } + } + } + } + + private void handleApnUnregistered(UnregisteredEvent event) { + logger.warn("Got APN Unregistered: " + event.getNumber() + "," + event.getDeviceId()); + + Optional account = accountsManager.get(event.getNumber()); + + if (account.isPresent()) { + Optional device = account.get().getDevice(event.getDeviceId()); + + if (device.isPresent()) { + if (event.getRegistrationId().equals(device.get().getApnId())) { + logger.warn("APN Unregister APN ID matches!"); + device.get().setApnId(null); + accountsManager.update(account.get()); + } + } + } + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/push/PushServiceClient.java b/src/main/java/org/whispersystems/textsecuregcm/push/PushServiceClient.java index 822cf42c2..7bb2ad347 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/push/PushServiceClient.java +++ b/src/main/java/org/whispersystems/textsecuregcm/push/PushServiceClient.java @@ -1,20 +1,29 @@ package org.whispersystems.textsecuregcm.push; import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.UniformInterfaceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.configuration.PushConfiguration; import org.whispersystems.textsecuregcm.entities.ApnMessage; import org.whispersystems.textsecuregcm.entities.GcmMessage; +import org.whispersystems.textsecuregcm.entities.UnregisteredEvent; +import org.whispersystems.textsecuregcm.entities.UnregisteredEventList; import org.whispersystems.textsecuregcm.util.Base64; import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.List; public class PushServiceClient { - private static final String PUSH_GCM_PATH = "/api/v1/push/gcm"; - private static final String PUSH_APN_PATH = "/api/v1/push/apn"; + private static final String PUSH_GCM_PATH = "/api/v1/push/gcm"; + private static final String PUSH_APN_PATH = "/api/v1/push/apn"; + + private static final String APN_FEEDBACK_PATH = "/api/v1/feedback/apn"; + private static final String GCM_FEEDBACK_PATH = "/api/v1/feedback/gcm"; private final Logger logger = LoggerFactory.getLogger(PushServiceClient.class); @@ -38,15 +47,41 @@ public class PushServiceClient { sendPush(PUSH_APN_PATH, message); } - private void sendPush(String path, Object entity) throws TransientPushFailureException { - ClientResponse response = client.resource("http://" + host + ":" + port + path) - .header("Authorization", authorization) - .entity(entity, MediaType.APPLICATION_JSON) - .put(ClientResponse.class); + public List getGcmFeedback() throws IOException { + return getFeedback(GCM_FEEDBACK_PATH); + } - if (response.getStatus() != 204 && response.getStatus() != 200) { - logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); - throw new TransientPushFailureException("Bad response: " + response.getStatus()); + public List getApnFeedback() throws IOException { + return getFeedback(APN_FEEDBACK_PATH); + } + + private void sendPush(String path, Object entity) throws TransientPushFailureException { + try { + ClientResponse response = client.resource("http://" + host + ":" + port + path) + .header("Authorization", authorization) + .entity(entity, MediaType.APPLICATION_JSON) + .put(ClientResponse.class); + + if (response.getStatus() != 204 && response.getStatus() != 200) { + logger.warn("PushServer response: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); + throw new TransientPushFailureException("Bad response: " + response.getStatus()); + } + } catch (UniformInterfaceException | ClientHandlerException e) { + logger.warn("Push error: ", e); + throw new TransientPushFailureException(e); + } + } + + private List getFeedback(String path) throws IOException { + try { + UnregisteredEventList unregisteredEvents = client.resource("http://" + host + ":" + port + path) + .header("Authorization", authorization) + .get(UnregisteredEventList.class); + + return unregisteredEvents.getDevices(); + } catch (UniformInterfaceException | ClientHandlerException e) { + logger.warn("Request error:", e); + throw new IOException(e); } }