Apple's Push Notification with Ruby EventMachine.

Apple’s Push Notification is a great way to complement your app and enhance your users experience even when your app is not running. One of the challenges in supporting Push Notification in your iPhone/iPad app is the communication with Apple Push Notification Service (APNs). Typically you might want your APNs Client running on separate dedicated server which may not be a Mac Platform and if you are doing large number of pushes every day you need a scalable client. There are several options in writing your APNs Client, but you can’t beat the beauty and the efficiency of the Ruby’s EventMachine library. The EventMachine library is fast, asynchronous and when combined with the elegance of Ruby, it makes it a joy to write programs.

In this post we will show how to write a simple APNs client using the Ruby EventMachine gem on any machine ( I have tested this script on Mac and Ubuntu Hardy, your mileage might vary on other systems).

To start off make sure you have the eventmachine gem install, if not:

sudo gem install eventmachine

Before you install eventmachine, make sure you have the “libssl-dev” package installed or else you might see an error like this:

Starting tls
    terminate called after throwing an instance of 'std::runtime_error'
    what():  Encryption not available on this event-machine
    Aborted

Next, follow the Apple’s Local and Push Notification Guide to create the dedicated Bundle ID, Sandbox and Production certificates to communicate with the APNs. After the certificates are created, download them on to your machine. The communication with the APNs is encrypted and the encryption is made possible by using the private key (that you used to create the certificate) and the certificate themselves. The certificate files are typically named as aps_developer_identity.cer aps_production_identity.cer respectively. Before you can use them with EventMachine library you need to convert them to “.pem” format. You can use the “openssl” tool accomplish that.

openssl x509 -inform der -in aps_developer_identity.cer -out aps_developer_identity.pem
openssl x509 -inform der -in aps_production_identity.cer -out aps_production_identity.pem 

The EventMachine Library also requires that the private key to be in the “.pem” format as well. On your Mac, Open up “KeyChain Access” and export the the private key. The only option to export the keys would in the “.p12” (Personal Information Exchange) format. Once you have the “.p12” file, run this openssl command to extract the private key:

openssl pkcs12 -nocerts -in mypk.p12 -out myaps_dev.key -nodes

The key also need to stripped of its passpharse to allow programmatic access:

openssl rsa -in myaps_dev.key -out myaps_dev_unenc.key

Now we will write a simple program that will communicate with the APNs with device token as the first argument and alert message as the second argument. This program uses the enhanced notification format versus simple format for the push notification since the error code/status is only available in the enhanced notification format.

    require 'rubygems'
    require 'eventmachine'
    require 'json'
    require 'logger'
    require 'pp'

    class PushClient < EventMachine::Connection

        def initialize *args
            @args=args
        end

        def post_init
            puts "Starting tls"
            start_tls(:private_key_file => '/Users/blah/tmp/pushnotify/myaps_dev_unenc.key', 
                    :cert_chain_file => '/Users/blah/tmp/pushnotify/aps_developer_identity.pem',
                      :verify_peer => false)
            puts "End Tls"
        end

        def ssl_handshake_completed
            puts "SSL handshake completed"
            payload = {}
            payload[:aps] = {}
            payload[:aps][:alert] = @args[1]
            jsonString = payload.to_json
            length = jsonString.length
            a= [1, 66, 0, 32, @args[0], length, jsonString]
            data = a.pack("cNNnH*na*")
            puts "Sending: #{data} With format array #{a.inspect} END"
            send_data data
        end

        def receive_data data
            puts "Received: #{data} END"
            arr = data.unpack("ccN")
            puts "Arr is: #{arr.inspect}"
        end

        def unbind
            puts "Closing connection"
            EventMachine::stop_event_loop
        end

    end

    EventMachine::run {
        EventMachine::connect "gateway.sandbox.push.apple.com", 2195, PushClient, *ARGV
    }

    

Go Ahead and run this program like:

./push_notify.rb "9d4555ecf27eb598dbaf735edfc154b4ad4f43205f4ede16e81dfcba1f9ccf21" "Test Push"

If successful, you should see the message on your iOS device or else you will get an error message and the APNs Server automatically closes the connection on an error. The error message will look something like:

Received:B END
Arr is: [8, 7, 66]
Closing connection

The second element of the array is the error code followed by the your payload’s unique identifier.

Now go forth and Push!.