iOS Push Notifications w/ Ruby

Alex Egg,

This page will document the steps to sending push notification to an iOS device in ruby.

First we need to collection the device token from the ios device and then we need get a certificate from apple and then we will use that to send the push from ruby.

Here is the client/server flow to get push tokens and send messages:

flow

##Get token

This is documented Ad nauseam elsewhere, however, It is summarized in the figure above and I’ll post a summary here.

In your app delegate call registerForRemoteNotificationTypes

  [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];

This will send a request to the APNS servers and register your phone and return a corresponding token with the method call didRegisterForRemoteNotificationsWithDeviceToken. You can use this token in the future to send request. If you are building a web app you must register the token w/ the server b/c the server is sending requests. Save the token in the database on the server.

On the server, to send a request, use the token w/ your certificate to send the message. These steps are documented below:

Certificate

You will need a certificate issued by WWDR’s iOS Provisioning Portal. After adding this certificate to your keychain,
please export it as a .p12 file and copy it to data/Certificate/anythingYouWant.p12. Remember the
password you choose. You’ll be asked for it later.

To configure push notifications for this iOS App ID, a Client SSL Certificate that allows your notification server to connect to the Apple Push Notification Service is required. Each iOS App ID requires its own Client SSL Certificate. Manage and generate your certificates below.

  1. Log into the developer portal click ‘App IDs’ in the in the ‘Identifiers’ Section.
  2. Select your app from the list
  3. Click the ‘Edit’ Button
  4. Ensure ‘Push Notifications’ is checked and create a certificate if you don’t have one. (Launch Keychain app > Certificate Assistant > Request Certificate from Certificate authority)
  5. Upload the your certificate into the from
  6. Apple will give you back a your new certificate (*.cer)
  7. Double click the cert to add it to keychain access
  8. Select the “My Certificates” filter on the bottom left
  9. Right mouse click your cert “Apple Development….” and click “Export …”
  10. Export as a .p12 file
  11. In you shell run:

It’s easier to work w/ the certificate in code if it’s in the .pem format.

openssl pkcs12 -in cert.p12 -out cert.pem -nodes -clcerts

Record the password you use in this step as you will need it to use the cert in your code.

Send push w/ pure ruby

require 'singleton'
require 'socket'
require 'openssl'

class APNS
  include Singleton
  attr_accessor :config, :certificate, :socket, :ssl_socket, :certificate_password

  def get_certificate_path
    cert_directory = File.absolute_path(File.dirname(File.expand_path(__FILE__)) + "/../certificate")
    certs = Dir.glob("#{cert_directory}/*.p12")
    if  certs.count ==0
	puts "Couldn't find a certificate at #{cert_directory}"
        puts "Exiting"
        Process.exit
    else
        certificate_path = certs[0]
    end
  end
  
  def initialize
    self.certificate_password = get_certificate_password
    self.certificate = load_certificate(get_certificate_path, self.certificate_password)
  end
  
  def get_certificate_password
    puts "Please enter your certificate password: "
    password_input = gets.chomp

    return password_input
  end
  
  def load_certificate(path, password=nil)
    context = OpenSSL::SSL::SSLContext.new
    context.verify_mode = OpenSSL::SSL::VERIFY_NONE
    
    # Import the certificate
    p12_certificate = OpenSSL::PKCS12::new(File.read(path), self.certificate_password)
    
    context.cert = p12_certificate.certificate
    context.key = p12_certificate.key
    
    # Return ssl certificate context
    return context
  end
  
  def open_connection(environment='sandbox')
    if self.certificate.class != OpenSSL::SSL::SSLContext

      load_certificate
    end
      puts "env: #{environment}"    
    if environment == "production"
      self.socket = TCPSocket.new("gateway.push.apple.com", 2195)
    else
      self.socket = TCPSocket.new("gateway.sandbox.push.apple.com", 2195)
    end
    self.ssl_socket = OpenSSL::SSL::SSLSocket.new(APNS.instance.socket, APNS.instance.certificate)

    # Open the SSL connection
    self.ssl_socket.connect
    
    
  end
  
  def close_connection
    APNS.instance.ssl_socket.close
    APNS.instance.socket.close
  end
  
  def deliver(token, payload)
    notification_packet = self.generate_notification_packet(token, payload)
    APNS.instance.ssl_socket.write(notification_packet)
  end
  
  def generate_notification_packet(token, payload)
    device_token_binary = [token.delete(' ')].pack('H*')
    
    packet =  [
                0,
                device_token_binary.size / 256,
                device_token_binary.size % 256,
                device_token_binary,
                payload.size / 256,
                payload.size % 256,
                payload
              ]
    packet.pack("ccca*cca*")
  end
  
  
end


The important parts are the load_certificate and generate_notification_packet methods.

In load_certificate we read our certificate and save it the ivar certificate (in initialize constructor). This is used to make the SSL connection to apple in open_connection.

The final other interesting method is generate_notification_packet which actually builds the binary packet that is sent over the socket. It’s kinda string building binary data structures in ruby but this is how you do it.

There are 3 versions of the apns protocol (as of now 8/21/2014), but i’ll just take a look at the first. The push notification packet protocol is thus:

The method argument token is a string. The routine pack('H*') interprets the string as hex numbers, two characters per byte, and converts it to a string with the characters with the corresponding ASCII code. e.g The hex code for a is 61, so ["61"].pack('H*') => "a"

We then build a packet where we prepend the token w/ the the length and prepend the payload with length

packet.pack("ccca*cca*")
 => "\x00\x00 \xF9\x13\xB6O\x12\xD73\xAF\xD4\x90\x9Ev\xC4\xFF\xE6M\x91jk\xB2\x8C\x0F<K\xF6\xCC\xC2y]\xADr;\x00\x10I am the payload"

This is then sent to apple over the ssl socket in deliver.

Send push w/ Houston Gem

Like I mentioned above, there are different protocol version, and it may be difficult to keep up with them when sending push notifications isn’t your core business. This is why I like to use a 3rd party library to send them out.

Here is an example w/ the Houston gem: (https://github.com/nomad/Houston)

apn push "f913b64f12d733afd4909e76c4ffe64d916a6bb28c0f3c4bf6ccc2795dad723b" -c cert.pem -m "i love you"

screen

##A note on 3rd party services like: Pusher/Urban Airship

As you can see there is a considerable server infrastructure required to support push notifications w/ Apple. Now consider if you want to integrate push w/ your Android clients, you have to reproduce the same type of infrastructure for Google’s Cloud push system. Now imagine implementing push’s with Windows phones, ad infinitum. Also, consider the case where you are simply a client-side developer and you didn’t plan to have a server component part of your infrastructure – you only need a client; In that case you can’t use pushes. This is the motivation for services like Urban Airship and Pusher: they are an abstraction layer over APNS GCM etc. If you just implement Pusher you get access all the services w/ 1 interface. Also, these services will also provide cloud storage (DB) for indie-developers who don’t want to manage a server.

These services provide a lot of functionality and Parse has very generous pricing so I would highly suggest using it than rolling out your own APNS implementation and I use it for all my projects.


Permalink: ios-push-notifications-w-ruby

Tags:

Last edited by Alex Egg, 2016-10-05 19:04:52
View Revision History