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:
