Push Notification and Python (Django)
I’ve been trying to get the iPhone Push notification services introduced in OS 3.0 working with Python (specifically, Django). It took a while but I figured I’d post my notes so it’ll spare someone the suffering I had to go through.
Other attempts at this (apns-python-wrapper (aka APNSWrapper) and Lee Packham’s code) both use the standard ssl libraries which I couldn’t get to work under Snow Leopard and Python 2.6.
I eventually got it going with Django and pyOpenSSL libraries. The easiest way is to get the prebuilt pyOpenSSL binaries from eGenix. If the test server is running on Snow Leopard the Python 2.6 UCS2 version seems to work best.
The thing about pyOpenSSL is that it expects the certificate and private key to be in separate .PEM files so you have to export each one from KeyChain Access in .P12 format then convert them to .PEM from the command line. For the development key it’s easier to specify the password during export then strip it out on the command line. In production you may not want to do that.
% openssl pkcs12 -clcerts -nokeys -out devcert.pem -in devcert.p12 % openssl pkcs12 -nocerts -out devkey.pem -in devkeypw.p12 % openssl rsa -in devkeypw.pem -out devkey.pemNow copy
devcert.pem and devkey.pem (with no password) into the server directory.
The following bit of code is in a file called PushSender.py and does the actual talking to the push server (I heavily edited out project-specific bits and standard exception and error-handling stuff. Hopefully didn’t bork it too badly
Also, in this example the actual communication runs in a thread. In some cases it might make more sense to run it as a main routine:
import os, sys
import struct, binascii, ssl, datetime
import threading
import simplejson as json
from socket import socket
from OpenSSL import SSL
class PushSender(threading.Thread):
def __init__(self, sandbox, token, message, badge, sound):
super(PushSender, self).__init__()
self.token = token
self.sandbox = sandbox
self.message = message
self.badge = badge
self.sound = sound
self.ctx = SSL.Context(SSL.SSLv3_METHOD)
if sandbox:
self.apnHost = "gateway.sandbox.push.apple.com"
self.ctx.use_certificate_file(os.path.join(PROJECT_ROOT, "devcert.pem"))
self.ctx.use_privatekey_file(os.path.join(PROJECT_ROOT, "devkey.pem"))
else:
self.apnHost = "gateway.push.apple.com"
self.ctx.use_certificate_file(os.path.join(PROJECT_ROOT, "prodcert.pem"))
self.ctx.use_privatekey_file(os.path.join(PROJECT_ROOT, "prodcert.pem"))
def run(self):
payload = {}
aps = {}
if (self.message):
aps["alert"] = str(self.message)
if (self.badge):
aps["badge"] = self.badge
if (self.sound):
aps["sound"] = str(self.sound)
payload["aps"] = aps
token = binascii.unhexlify(self.token)
payloadstr = json.dumps(payload, separators=(',',':'))
payloadLen = len(payloadstr)
fmt = "!cH32sH%ds" % payloadLen
command = '\x00'
msg = struct.pack(fmt, command, 32, token, payloadLen, payloadstr)
sock = socket()
s = SSL.Connection(self.ctx, sock)
s.connect((self.apnHost, 2195))
s.send(msg)
s.shutdown()
s.close()
To invoke it in a thread (the first param is True for sandbox and False for production server) from the main routine:
from PushSender import PushSender ... pushsender = PushSender(True, token, pushmessage, pushbadge, pushsound) pushsender.start()On the phone the token returned by the
didRegisterForRemoteNotificationsWithDeviceToken method is in the form <xxxxxxxx xxxxxxxx ...>. To pass it along to the server you have to strip out the <> and spaces. The unhexlify method then converts this string into a 32-byte hex binary value. The easiest way to strip out the extraneous stuff is to just do it on the client-side:
NSString *deviceToken = [[[[tokenString description]
stringByReplacingOccurrencesOfString:@"< " withString:@""]
stringByReplacingOccurrencesOfString:@">" withString:@""]
stringByReplacingOccurrencesOfString: @" " withString: @""];
One last thing: at least with the sandbox if the phone is running on WiFi sometimes push notices don’t come through. This may be due to router NAT or firewall configuration issues. One way to check is to go into XCode Organizer while the phone is tethered and run your app, then under the Organizer Console you can check for errors. To get around this you’ll want to turn off the WiFi on the phone and go with 3G. This usually makes push notices arrive as expected (and strangely enough the WiFi method goes back to working for a few minutes).
There’s more that needs to be done to make it production-ready but at least it’s good to know it’s doable:

Thanks for sharing!
It is a nice little script for the development phase.
William
24 Sep 09 at 8:35 pm
Interesting – I just saw this and wanted to say that I am using Python 2.6 on OSX 10.6 (Snow Leopard) and am able to use the standard SSL libraries. Would be interested to know what trouble you ran into.
Lee Packham
29 Sep 09 at 3:24 am
HI Lee, thanks for the note.
The problem with debugging Push is that you get practically no feedback from Apple’s service. The feedback mechanism is practically useless for debugging, so the trouble I ran into manifested itself in the packet being sent to the Push Notification Service with no errors on the sender side … and nothing happening on the phone.
What ended up working was using the OpenSSL libraries and keeping the private key and certificate files separate (instead of merging them together). I now get 100% reliability with the above script (at least in the sandbox).
ramin
29 Sep 09 at 7:46 pm
Will this nor work if used on a Linux server?
Bob
28 Nov 09 at 11:39 am