Repository: relayrides/pushy (java)

Issue:
MDM push notifications
Hi,

I'm trying to implement MDM functionalities in an app. But for some reason, I'm receiving `DeviceTokenNotForTopic`.

The topic is something like `com.apple.mgmt.External.{uuid}` and is correct and also the token. Let's go to the code.

```
try {
            Future<Void> connectFuture = apnsClient.connect(ApnsClient.PRODUCTION_APNS_HOST);
            connectFuture.await();
            String token = decodeTokenFromBase64(mdm.getToken());
            SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, configurationFacade.getString(Constants.MDM_TOPIC), createPayload(mdm.getPushMagic()));
            Future<PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture = apnsClient.sendNotification(pushNotification);
            PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse = sendNotificationFuture.get();
            if (pushNotificationResponse.isAccepted()) {
                LOGGER.info("environment:{}|deviceId:{}|MDM Push notification accepted by APNs gateway.", ApnsClient.PRODUCTION_APNS_HOST, device.getId());
            } else {
                LOGGER.info("environment:{}|deviceId:{}|MDM Notification rejected by the APNs gateway: {}", ApnsClient.PRODUCTION_APNS_HOST, device.getId(), pushNotificationResponse.getRejectionReason());
                if (pushNotificationResponse.getTokenInvalidationTimestamp() != null) {
                    LOGGER.info("environment:{}|deviceId:{}|The mdm token is invalid as of: {}", ApnsClient.PRODUCTION_APNS_HOST, device.getId(), pushNotificationResponse.getTokenInvalidationTimestamp());
                }
            }
            LOGGER.debug("userId:{}|deviceId:{}|wake up mdm notification sent", user.getId(), device.getId());
        } catch (InterruptedException | ExecutionException ex) {
            LOGGER.warn("environment:{}|deviceId:{}|sending mdm push notification failed. ", configurationFacade.getString(Constants.MDM_HOST), device.getId(), ex.getMessage());
        }
```
**Create Payload**

```
private String createPayload(String pushMagic) {
        ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
        payloadBuilder.addCustomProperty("mdm", pushMagic);
        return payloadBuilder.buildWithDefaultMaximumLength();
    }
```

**Decode Token**
```
private String decodeTokenFromBase64(String tokenFromPayload) {
        byte[] bytes = Base64.decodeBase64(tokenFromPayload.getBytes());
        return Hex.encodeHexString(bytes);
    }
```

**Push Notification sent:**
SimpleApnsPushNotification 

```
[token=xxcc33xxxxxfe46bxxxxx225c70c8xxxxxfc42065c1cxxxxx18cfc32d8xxxxx, payload={"aps":{},"mdm":"XXXXFDB1-XXXX-42F8-XXXX-3078E66EXXXX"}, invalidationTime=null, priority=IMMEDIATE, topic=com.apple.mgmt.External.7cfxxxxe-661e-xxxx-b5xx-xxxx80ddxxxx, apns-collapse-id=null]
```

**Error**
environment:api.push.apple.com|deviceId:41|MDM Notification rejected by the APNs gateway: DeviceTokenNotForTopic

Can someone explain me what I'm doing wrong? Pushy supports MDM?

NOTE: Before using pushy I used the notnoop and it worked perfectly, I'm planning on switching to pushy but this issue is blocking me.


**EDIT:** After some reading in the [MDM documentation](https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW2) I found this: 

> In place of PushMagicValue above, substitute the actual PushMagic string that the device sends to the MDM server in the TokenUpdate message. That should be the whole message. There should not be an aps key. (The aps key is used only for third-party app push notifications.)
I printed the push notification and the aps went to the APNs with empty value "aps":{} 
The problem can arise from this? How can I remove the aps?

**EDIT2:** I've made some changes in the Pushy client to not includethe aps on the payload and didn't work either. The payload now follows this structure `{"mdm": "UDID"}`
I've made a Clone and created a branch, if you need I can make a pull request for you to test. I must warn you that the code was a quick fix and not the final version I would like to see to solve this case.


Thanks in advance,


Pull request description:
Add support for building MDM "wake up" payloads
This is an alternate approach to constructing MDM "wake up" payloads as discussed in #509 and #511. This closes #509.

Patch:
diff --git a/pushy/src/main/java/com/turo/pushy/apns/util/ApnsPayloadBuilder.java b/pushy/src/main/java/com/turo/pushy/apns/util/ApnsPayloadBuilder.java
index af7bf5ff..c192c6e5 100644
--- a/pushy/src/main/java/com/turo/pushy/apns/util/ApnsPayloadBuilder.java
+++ b/pushy/src/main/java/com/turo/pushy/apns/util/ApnsPayloadBuilder.java
@@ -22,16 +22,13 @@
 
 package com.turo.pushy.apns.util;
 
-import java.io.CharArrayWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
+import java.io.CharArrayWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
 /**
  * <p>A utility class for constructing JSON payloads suitable for inclusion in APNs push notifications. Payload builders
  * are reusable, but are <em>not</em> thread-safe.</p>
@@ -92,11 +89,13 @@ public class ApnsPayloadBuilder {
     private static final String ACTION_LOC_KEY = "action-loc-key";
     private static final String LAUNCH_IMAGE_KEY = "launch-image";
 
+    private static final String MDM_KEY = "mdm";
+
     private final HashMap<String, Object> customProperties = new HashMap<>();
 
     private static final String ABBREVIATION_SUBSTRING = "…";
 
-    private static final Gson gson = new GsonBuilder().serializeNulls().disableHtmlEscaping().create();
+    private static final Gson GSON = new GsonBuilder().serializeNulls().disableHtmlEscaping().create();
 
     /**
      * The name of the iOS default push notification sound
@@ -659,7 +658,7 @@ public class ApnsPayloadBuilder {
         }
 
         this.buffer.reset();
-        gson.toJson(payload, this.buffer);
+        GSON.toJson(payload, this.buffer);
 
         final String payloadString = this.buffer.toString();
         final int initialPayloadSize = payloadString.getBytes(StandardCharsets.UTF_8).length;
@@ -673,7 +672,7 @@ public class ApnsPayloadBuilder {
                 this.replaceMessageBody(payload, "");
 
                 this.buffer.reset();
-                gson.toJson(payload, this.buffer);
+                GSON.toJson(payload, this.buffer);
 
                 final int payloadSizeWithEmptyMessage = this.buffer.toString().getBytes(StandardCharsets.UTF_8).length;
 
@@ -690,7 +689,7 @@ public class ApnsPayloadBuilder {
                 this.replaceMessageBody(payload, fittedMessageBody + ABBREVIATION_SUBSTRING);
 
                 this.buffer.reset();
-                gson.toJson(payload, this.buffer);
+                GSON.toJson(payload, this.buffer);
 
                 fittedPayloadString = this.buffer.toString();
             } else {
@@ -703,6 +702,25 @@ public class ApnsPayloadBuilder {
         return fittedPayloadString;
     }
 
+    /**
+     * Returns a JSON representation of a
+     * <a href="https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/1-Introduction/Introduction.html#//apple_ref/doc/uid/TP40017387-CH1-SW1">Mobile
+     * Device Management</a> "wake up" payload.
+     *
+     * @param pushMagicValue the "push magic" string that the device sends to the MDM server in a {@code TokenUpdate}
+     * message
+     *
+     * @return a JSON representation of an MDM "wake up" notification payload
+     *
+     * @see <a href="https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html#//apple_ref/doc/uid/TP40017387-CH3-SW2">Mobile
+     * Device Management (MDM) Protocol</a>
+     *
+     * @since 0.12
+     */
+    public static String buildMdmPayload(final String pushMagicValue) {
+        return GSON.toJson(java.util.Collections.singletonMap(MDM_KEY, pushMagicValue));
+    }
+
     private void replaceMessageBody(final Map<String, Object> payload, final String messageBody) {
         @SuppressWarnings("unchecked")
         final Map<String, Object> aps = (Map<String, Object>) payload.get(APS_KEY);


Test patch:
diff --git a/pushy/src/test/java/com/turo/pushy/apns/util/ApnsPayloadBuilderTest.java b/pushy/src/test/java/com/turo/pushy/apns/util/ApnsPayloadBuilderTest.java
index 3c551446..1b22da5d 100644
--- a/pushy/src/test/java/com/turo/pushy/apns/util/ApnsPayloadBuilderTest.java
+++ b/pushy/src/test/java/com/turo/pushy/apns/util/ApnsPayloadBuilderTest.java
@@ -22,9 +22,11 @@
 
 package com.turo.pushy.apns.util;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import org.junit.Before;
+import org.junit.Test;
 
 import java.lang.reflect.Type;
 import java.nio.charset.CharsetEncoder;
@@ -33,23 +35,17 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
-import org.junit.Before;
-import org.junit.Test;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.reflect.TypeToken;
+import static org.junit.Assert.*;
 
 public class ApnsPayloadBuilderTest {
 
     private ApnsPayloadBuilder builder;
-    private Gson gson;
 
-    private static Type MAP_OF_STRING_TO_OBJECT = new TypeToken<Map<String, Object>>(){}.getType();
+    private static final Gson GSON = new GsonBuilder().serializeNulls().disableHtmlEscaping().create();
+    private static final Type MAP_OF_STRING_TO_OBJECT = new TypeToken<Map<String, Object>>(){}.getType();
 
     @Before
     public void setUp() {
-        this.gson = new GsonBuilder().serializeNulls().disableHtmlEscaping().create();
         this.builder = new ApnsPayloadBuilder();
     }
 
@@ -429,7 +425,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.addCustomProperty(customKey, customValue);
 
         @SuppressWarnings("unchecked")
-        final Map<String, Object> payload = this.gson.fromJson(
+        final Map<String, Object> payload = GSON.fromJson(
                 this.builder.buildWithDefaultMaximumLength(), MAP_OF_STRING_TO_OBJECT);
 
         assertEquals(customValue, payload.get(customKey));
@@ -553,7 +549,7 @@ public class ApnsPayloadBuilderTest {
             if (encoder.canEncode((char) codePoint)) {
                 // We subtract 2 here for the quotes that will appear on either end of a JSON string
                 assertEquals("Escaped/encoded lengths should match for code point " + codePoint,
-                        this.gson.toJson(String.valueOf((char) codePoint)).getBytes(StandardCharsets.UTF_8).length - 2,
+                        GSON.toJson(String.valueOf((char) codePoint)).getBytes(StandardCharsets.UTF_8).length - 2,
                         ApnsPayloadBuilder.getSizeOfJsonEscapedUtf8Character((char) codePoint));
             }
         }
@@ -572,9 +568,14 @@ public class ApnsPayloadBuilderTest {
         assertEquals(2, ApnsPayloadBuilder.getLengthOfJsonEscapedUtf8StringFittingSize("\n\tThis string has escaped characters.", 4));
     }
 
+    @Test
+    public void testBuildMdmPayload() {
+        assertEquals("{\"mdm\":\"Magic!\"}", ApnsPayloadBuilder.buildMdmPayload("Magic!"));
+    }
+
     @SuppressWarnings("unchecked")
     private Map<String, Object> extractApsObjectFromPayloadString(final String payloadString) {
-        final Map<String, Object> payload = this.gson.fromJson(payloadString, MAP_OF_STRING_TO_OBJECT);
+        final Map<String, Object> payload = GSON.fromJson(payloadString, MAP_OF_STRING_TO_OBJECT);
         return (Map<String, Object>) payload.get("aps");
     }
 }