I have recently started doing development on the iPhone. It’s great fun. I am particularly interested in the Push aspect of this. I’m also a big fan of Python.

There’s a number of RESTful things that can be done with Ruby on Rails. However, the whole ‘packaged application’ thing is quite new to that framework. To Django, it’s the staple diet of how to get things done. So I’m currently part way through a generic application for Django for sending APN (Apple Push Notification) requests as well as dealing with the feedback connection.

Currently I have the push side working quite well. Just a warning, this is my first stab at this and it requires Python 2.6, or the relevant backports for ssl and json installed on 2.5.

from django.db import models
from django.conf import settings

from socket import socket

import datetime
import struct
import ssl
import binascii
import json

class iPhone(models.Model):
    """
    Represents an iPhone used to push

    udid - the iPhone Unique Push Identifier (64 chars of hex)
    last_notified_at - when was a notification last sent to the phone
    test_phone - is this a phone that should be included in test runs
    notes - just a small notes field so that we can put in things like "Lee's iPhone"
    failed_phone - Have we had feedback about this phone? If so, flag it.
    """
    udid = models.CharField(blank=False, max_length=64)
    last_notified_at = models.DateTimeField(blank=True, default=datetime.datetime.now)
    test_phone = models.BooleanField(default=False)
    notes = models.CharField(blank=True, max_length=100)
    failed_phone = models.BooleanField(default=False)

    class Admin:
        list_display = ('',)
        search_fields = ('',)

    def send_message(self, alert, badge=0, sound="chime", sandbox=True,
                        custom_params={}, action_loc_key=None, loc_key=None,
                        loc_args=[], passed_socket=None):
        """
        Send a message to an iPhone using the APN server, returns whether
        it was successful or not.

        alert - The message you want to send
        badge - Numeric badge number you wish to show, 0 will clear it
        sound - chime is shorter than default! Replace with None/"" for no sound
        sandbox - Are you sending to the sandbox or the live server
        custom_params - A dict of custom params you want to send
        action_loc_key - As per APN docs
        loc_key - As per APN docs
        loc_args - As per APN docs, make sure you use a list
        passed_socket - Rather than open/close a socket, use an already open one

        This requires IPHONE_APN_PUSH_CERT in settings.py to be the full
        path to the cert/pk .pem file.
        """
        aps_payload = {}

        alert_payload = alert
        if action_loc_key or loc_key or loc_args:
            alert_payload = {'body' : alert}
            if action_loc_key:
                alert_payload['action-loc-key'] = action_loc_key
            if loc_key:
                alert_payload['loc-key'] = loc_key
            if loc_args:
                alert_payload['loc-args'] = loc_args

        aps_payload['alert'] = alert_payload

        if badge:
            aps_payload['badge'] = badge

        if sound:
            aps_payload['sound'] = sound        

        payload = custom_params
        payload['aps'] = aps_payload

        s_payload = json.dumps(payload, separators=(',',':'))

        fmt = "!cH32sH%ds" % len(s_payload)
        command = '\x00'
        msg = struct.pack(fmt, command, 32, binascii.unhexlify(self.udid), len(s_payload), s_payload)

        if passed_socket:
            passed_socket.write(msg)
        else:
            host_name = 'gateway.sandbox.push.apple.com' if sandbox else 'gateway.push.apple.com'
            s = socket()
            c = ssl.wrap_socket(s,
                                ssl_version=ssl.PROTOCOL_SSLv3,
                                certfile=settings.IPHONE_APN_PUSH_CERT)
            c.connect((host_name, 2195))
            c.write(msg)
            c.close()

        return True

    def __unicode__(self):
        return u"iPhone %s" % self.udid

So how do we use it? Well rather easy. First you need to set up your certificates with Apple. You’ll need to set up a specific AppID and provisioning profile for your application (i.e. you can’t use *).

The guide on Apple’s site covers how to do this. Python needs it in a combined PEM format to work. Other sites claim you have to export from Keychain Access and that you’ll need to convert both to .pem. I only had to convert the private key:

openssl pkcs12 -in pkey.p12 -out pkey.pem -nodes -clcerts
cat cert.pem pkey.pem > iphone_ck.pem

I then altered my settings.py to have the new entry I added:

IPHONE_APN_PUSH_CERT = os.path.join(PROJECT_ROOT, "iphone_ck.pem")

Note the full path. I always have this at the top of settings.py to make my like easier:

import os
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))

Now, it’s just case of making your iPhone app register with push and getting a unique ID. You have to use your real phone to do this. If you’re in a hurry and just want to test it out you can cheat and just pop this in applicationDidFinishLaunching delegate:

	// Register for push notifications
	[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)];

Then you probably want this to get the ID out into the console:

- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
	// Registration was successful so we'll
	// set up our device token etc.
	deviceToken = devToken;
	NSLog(@"devToken=%@",deviceToken);
    self.registered = YES;
}

- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
	// This is expected on the emulator so that's fine
    NSLog(@"Error in registration. Error: %@", err);
}

Then you’ll be good to go! Run the app on the phone and the console will output the ID. Created an iPhone object and send a message. Job done.

10 Responses to “Push on the iPhone”

  1. Richard Madders 16. Aug, 2009 at 7:48 pm #

    Hey,

    Great post. I’m struggling with one line though: self.registered = YES.

    This throws a compile error, “request for member ‘registered’ is something not in a structure or union”

    Any ideas most welcome!

    Richard

  2. Anish Kumar 17. Aug, 2009 at 9:44 am #

    Hi,

    Is there any success in getting feedback from APNS for devices failed to receive the notification? I am successfully able to connect to the feedback server and I did a fread. But fread() always returns me blank :-(

    Thanks,
    -Anish

  3. Lee 17. Aug, 2009 at 9:45 am #

    @ Anish – I have never got any feedback on either live or sandbox. Not sure what’s going on there to be honest.

  4. Anish Kumar 25. Sep, 2009 at 1:40 pm #

    finally i got the feedback service working on my production environment. I get some unreadable (hex)characters as output from the feedback service. I convert those to a string. But the converted string doesn’t seem to be matching with the device token of the iPhone. Need to dig more into it.

    -Anish

  5. Lee 25. Sep, 2009 at 1:46 pm #

    Ahhh wow – ace! I’ll connect to it later and dump the output from it. Remember that there are headers to each entry (but no footer) – could that be what you’re missing?

  6. ozgurv 22. Jan, 2010 at 12:01 am #

    Hi,

    what is “self.registered = YES” ?

    it causes an error.

  7. ozgurv 22. Jan, 2010 at 12:08 am #

    The callback methods never gets executed. Here is my code.

    - (void)applicationDidFinishLaunching:(UIApplication *)application {
    NSLog(@”Registering Remote Notications”);

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

    // Override point for customization after app launch
    [window makeKeyAndVisible];
    }

    // Delegation methods
    - (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
    const void *devTokenBytes = [devToken bytes];
    // self.registered = YES;
    NSLog(@”deviceToken: %@”, devToken);
    // [self sendProviderDeviceToken:devTokenBytes]; // custom method
    }

    - (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
    NSLog(@”Error in registration. Error: %@”, err);
    }

  8. ozgurv 22. Jan, 2010 at 12:09 am #

    And I did not get any error message from console. I successfully create my certificate and provisioning profile and btw I am testing on the phone.

  9. Lee 22. Jan, 2010 at 6:13 am #

    self.registered is an NSString on the class and is in the .h file – the code assumes the reader and make that themselves.

    You pasted your code, but I’m not sure you’re even calling it? The code has to be in the application instance for your app – otherwise it will not work.

    You also have to make sure you set up your provisioning properly – this probably the most common error on the phone side. The app requires it’s own provisioning profile and development certificate from Apple – to even test.

Trackbacks/Pingbacks

  1. 使用 python/django 发送iphone push消息 « 不落不乖 - 10. Dec, 2009

    [...] http://leenux.org.uk/2009/07/14/push-on-the-iphone/ [...]