Talk Funnel

Ramin Firoozye's (occasional) Public Whisperings

Push Notification and Python (Django)

Posted by: Ramin on September 9, 2009

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.pem

Now 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()

[ PushSender source ]

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:

cp18.png