Apple Push Notifications with Node.js

When your iPhone app backend  needs to send Apple Push Notifications, it must do this over raw SSL socket using Apple proprietary raw binary interface. Standard Web REST is not supported. This kind of sucks, because if your entire backend is web based you need to break that cleanliness with external HTTP to APN proxy. One option is to use services like Urban Airship, but you can also build the proxy by yourself.

One potential platform for this is hyped Node.js, the rising javascript engine for building ad-hoc web servers. Web is full of examples of building simple HTTP based server or proxy with Node.js, so this post is only the part where we open a secure connection to the Apple server and send push notifications with plain Node.js javascript.

Please note that Apple assumes that you pool and keep sockets open as long as you have notifications to send. So, don’t make naive implementation that makes new socket for each HTTP request. Some simple pooling and reuse is a must for real implementation.

In addition for sending the push notifications, your app also needs to poll the APNS feedback service to find out what devices have uninstalled the app and should not be pushed new notifications. See more details in post Apple Push Notification feedback service.

1. Get Certificates

Apple’s Push notification server authenticates application by SSL certificates. There is no additional authentication handshake after secure connection has been established.

First we need the PEM format certificates that you can get by exporting  them with Apple Keytool. Export also the Apple Worldwide CA certificate. See this excellent blog post (up to step 5)  for details how to acquire the PEM files: http://blog.boxedice.com/2010/06/05/how-to-renew-your-apple-push-notification-push-ssl-certificate/

Now you should have following certificate files.

  • app-cert.pem  (Application cerificate)
  • app-key-noenc.pem  (Application private key)
  • apple-worldwide-certificate-authority.cer  (Apple CA certificate)

2. Open Connection to Push Server

UPDATE:See more complete TLS example here.

Moving on the actual implementation in Node.js. This is quite simple, you just read the various certificate files as string and use them as credentials.

You must also have SSL support built in your Node.js binary.

var fs = require('fs');
var crypto = require('crypto');
var tls = require('tls');

var certPem = fs.readFileSync('app-cert.pem', encoding='ascii');
var keyPem = fs.readFileSync('app-key-noenc.pem', encoding='ascii');
var caCert = fs.readFileSync('apple-worldwide-certificate-authority.cer', encoding='ascii');
var options = { key: keyPem, cert: certPem, ca: [ caCert ] }

function connectAPN( next ) {
    var stream = tls.connect(2195, 'gateway.sandbox.push.apple.com', options, function() {
        // connected
        next( !stream.authorized, stream );
    });
}

3. Write Push Notification

After secure connection is established, you can simply write push notifications to the socket as binary data. Push notification is addressed to a device with 32 byte long push token that must be acquired by your iPhone application and sent to your backend somehow.

Easy format is simple hexadecimal string, so we define first a helper method to convert that hexadecimal string to binary buffer at server side.

function hextobin(hexstr) {
   buf = new Buffer(hexstr.length / 2);
   for(var i = 0; i < hexstr.length/2 ; i++) {
      buf[i] = (parseInt(hexstr[i * 2], 16) << 4) + (parseInt(hexstr[i * 2 + 1], 16));
   }
   return buf;
 }

Then define the data you want to send. The push payload is a serialized JSON string, that has one mandatory property ‘aps’. The JSON may contain additionally application specific custom properties.

var pushnd = { aps: { alert:'This is a test' }};
// Push token from iPhone app. 32 bytes as hexadecimal string
var hextoken = '85ab4a0cf2 ... 238adf';  

Now we can construct the actual push binary PDU (Protocol Data Unit). Note that payload length is encoded UTF-8 string length, not number of characters. This would be also good place to check the maximum payload length (255 bytes).

payload = JSON.stringify(pushnd);
var payloadlen = Buffer.byteLength(payload, 'utf-8');
var tokenlen = 32;
var buffer = new Buffer(1 +  4 + 4 + 2 + tokenlen + 2 + payloadlen);
var i = 0;
buffer[i++] = 1; // command
var msgid = 0xbeefcace; // message identifier, can be left 0
buffer[i++] = msgid >> 24 & 0xFF;
buffer[i++] = msgid >> 16 & 0xFF;
buffer[i++] = msgid >> 8 & 0xFF;
buffer[i++] = msgid > 0xFF;

// expiry in epoch seconds (1 hour)
var seconds = Math.round(new Date().getTime() / 1000) + 1*60*60;
buffer[i++] = seconds >> 24 & 0xFF;
buffer[i++] = seconds >> 16 & 0xFF;
buffer[i++] = seconds >> 8 & 0xFF;
buffer[i++] = seconds > 0xFF;

buffer[i++] = tokenlen >> 8 & 0xFF; // token length
buffer[i++] = tokenlen & 0xFF;
var token = hextobin(hextoken);
token.copy(buffer, i, 0, tokenlen)
i += tokenlen;
buffer[i++] = payloadlen >> 8 & 0xFF; // payload length
buffer[i++] = payloadlen & 0xFF;

var payload = Buffer(payload);
payload.copy(buffer, i, 0, payloadlen);

stream.write(buffer);  // write push notification

And that’s it.

4. Handling Error Messages

Apple does not return anything from the socket unless there was an error.  In that case Apple server sends you single binary error message with reason code (offending message is identified by the message id you set in push message)  and closes connection immediately after that.

To parse error message. Stream encoding is utf-8, so we get buffer instance as data argument.

stream.on('data', function(data) {
   var command = data[0] & 0x0FF;  // always 8
   var status = data[1] & 0x0FF;  // error code
   var msgid = (data[2] << 24) + (data[3] << 16) + (data[4] << 8 ) + (data[5]);
   console.log(command+':'+status+':'+msgid);
 }

This implementation assumes that all data (6 bytes) is received on single event. In theory Node.js might return data in smaller pieces.

5. Reading Apple Feedback notifications

Apple requires that you read feedback notifications daily, so you know what push tokens have expired or app was uninstalled. See this blog post Polling Apple Push Notification feedback service with Node.js for details.

20 Responses to Apple Push Notifications with Node.js

  1. Kevin says:

    Where does tokenlen get defined?

  2. sucram says:

    the json string should start with aps like { aps: { alert:'This is a test' }}; and not apn

  3. Dave Greenstein says:

    Hi Teemu, thank you for the post. I’m having an issue sending Emoji characters via node.js to the APN service. It works when you use the old softbank encodings, for example ‘\ue415’ for a smiley face, but not when you use the newer unified encoding, for example ‘\uD83D\uDE04’ for the same smiley face. JSON.stringify as well as encodeURI/decodeURI seems to encode/decode the unicode correctly. Not sure if it’s being rejected by the APN service. Have you run into this with your experience using APN on node?

    Thank you!
    Dave

    • tikonen says:

      I haven’t ever had any troubles with Emoji, but I don’t know if I’ve ever sent them unified encoding formatted. It’s possible that Node.js utf8 encoder or the APN service decoder is not up to the task, have you tried to use UCS2?

      • David G says:

        Yep. That was the issue. The wider characters requires UTF16 encoding. Unfortunately that halves the number of chars you can send in the 256byte payload… But problem solved.

  4. Pingback: Polling Apple Push Notification feedback service with Node.js « Brave New Method

  5. Hi, Where do I get the apple-worldwide-certificate-authority.cer?

    • tikonen says:

      Export it with Keychain Access, you can find it from /Applications/Utilities.
      Look up ‘Apple Worldwide Developer Relations Certification Authority’, right click and select export.

  6. Pingback: Apple Push Notifications with Haskell « Brave New Method

  7. ilongin says:

    Does anyone knows how to send one notification to multiple users in one connection?

    • tikonen says:

      Just keep connection open and rebuild the message for each push token separately and send it. Apple push server will not close connection until on error or timeout.

  8. Pingback: Sending iOS push notifications with NodeJS | Adam Putinski

  9. Daniel says:

    Hi, i have a problem, The tutorial works only in development mode, i uploaded my application to appstore and the push notification doesn’t work. Do you think the reason is because of the .p12 files? I created the .p12 files from development certificate and from the production certificates and the results were the same.

  10. Daniel says:

    This is my error message:
    8:8:-1091581234

  11. Daniel says:

    I realize the problem, you should specify this:

    Sandbox: gateway.sandbox.push.apple.com, port 2195. (for the development)
    Production: gateway.push.apple.com, port 2195. (for the release)

    • tikonen says:

      Glad you got it worked out, different hosts and certificates for sandbox and production are confusing.

  12. Pingback: Push notifications: tips and tricks

  13. Pingback: Simple test server for Apple push notification and feedback integration | Brave New Method

Leave a comment