Skip to content

Instantly share code, notes, and snippets.

@jancajthaml
Created July 23, 2014 08:50
Show Gist options
  • Save jancajthaml/72395d02be5c3964ece7 to your computer and use it in GitHub Desktop.
Save jancajthaml/72395d02be5c3964ece7 to your computer and use it in GitHub Desktop.
Python APN Client
#############################################
# Info backend requirements:
# - device token hash saved in database
# - number of unred notifications for token hash
# - Apple Certification File
# - SSL Key
#
# Info communication requirements:
# - what kind of notification
# - json data
# - number of unred notifications for badge purposes
#
# Good practices and Service Warnings:
# - Keep conection to "Apple servers" (APN servers) always open for ASAP push
# - Send notifications in Frames (push multiple notifications at once) (frame will be implemented)
# - APN does not save multiple notifications, they save only the last one (if the client is offline)
# - Feedback server tells us whetever a notification was recieved
#
#############################################
# Quick and Dirty DRAFT for Apple Push Notification REST by Petnik
from binascii import a2b_hex, b2a_hex
from datetime import datetime
from socket import socket, timeout, AF_INET, SOCK_STREAM
from socket import error as socket_error
from struct import pack, unpack
import struct
import sys
import ssl
import select
import time
import collections, itertools
import logging
try:
from ssl import wrap_socket, SSLError
except ImportError:
from socket import ssl as wrap_socket, sslerror as SSLError
from _ssl import SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE
try:
import json
except ImportError:
import simplejson as json
#############################################
certification_file = 'Certificate.pem' # Apple Certification (SSL)
key_file = 'Key.pem' # Key (SSL)
#############################################
# APN HELPERS START
#############################################
##
def read_chunk() :
while 1 :
data = feedback.read(4096)
yield data
if not data: break
##
def read_feedback() :
buff = ''
for chunk in read_chunk():
buff += chunk
if not buff or len(buff) < 6 : break # Sanity check
while len(buff) > 6:
bytes_to_read = 6 + unpack('>H', buff[4:6])[0]
if len(buff) >= bytes_to_read:
yield ( b2a_hex(buff[6:bytes_to_read]) , datetime.utcfromtimestamp(unpack('>I', buff[0:4])[0]) )
buff = buff[bytes_to_read:] # Remove data for current token from buffer
else : break
##
def connect_ssl( server = None , port = None , timeout = 3600 , enhanced = False) :
result = None
for i in xrange(3) : # Fallback for 'SSLError: _ssl.c:489: The handshake operation timed out'
try :
_socket = socket(AF_INET, SOCK_STREAM)
_socket.settimeout(timeout)
_socket.connect((server, port))
break
except timeout : pass
except : raise
if enhanced :
_socket.setblocking(False)
result = wrap_socket(_socket, certification_file, key_file, do_handshake_on_connect=False)
while 1 :
try :
_ssl.do_handshake()
break
except ssl.SSLError, err :
if ssl.SSL_ERROR_WANT_READ == err.args[0] : select.select([_ssl], [], [])
elif ssl.SSL_ERROR_WANT_WRITE == err.args[0] : select.select([], [_ssl], [])
else : raise
else :
for i in xrange(3) : # Fallback for 'SSLError: _ssl.c:489: The handshake operation timed out'
try :
result = wrap_socket(_socket, certification_file, key_file)
break
except SSLError, ex :
if ex.args[0] == SSL_ERROR_WANT_READ : sys.exc_clear()
elif ex.args[0] == SSL_ERROR_WANT_WRITE : sys.exc_clear()
else : raise
return result
##
#############################################
# APN HELPERS END
#############################################
#print b2a_hex(a2b_hex(a.replace(' ','')))
#print b2a_hex(a)
#_binary = a2b_hex(a)
#_hex = b2a_hex(_binary)
#if not a == _hex : print "SHIT"
#else : print "K"
#Took from Apple APN Documentation Chapter 3
MAX_PAYLOAD_LENGTH = 256
NOTIFICATION_FORMAT = (
'!' # network big-endian
'B' # command
'H' # token length
'32s' # token
'H' # payload length
'%ds' # payload
)
ENHANCED_NOTIFICATION_FORMAT = (
'!' # network big-endian
'B' # command
'I' # identifier
'I' # expiry
'H' # token length
'32s' # token
'H' # payload length
'%ds' # payload
)
ERROR_RESPONSE_FORMAT = (
'!' # network big-endian
'B' # command
'B' # status
'I' # identifier
)
#############################################
# these are iPhone device tokens they need to be stored in database
device_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
# Send a notification
payload = {
'aps':
{
'alert' : 'Kopity kop', # notification message to be displayed on screen
'sound' : 'k1DiveAlarm.caf', # audio feedback of notification ( 'default' for device default ), need dict
'badge' : 1, # number of overall unred notifications (server need to keep this number for client)
#'content-available' : 1, # dont know what this shit does
#'category' : '' # dont know what this shit does and probably need some dict
}
}
custom_data = {
'test_data':
{
'foo' : 'bar'
}
}
payload.update(custom_data)
##
# Create connection using the cert and pk saved locally, using sandbox for debug purpose (BAN PREVENTION)
gateway = connect_ssl( 'gateway.sandbox.push.apple.com' , 2195 ) #'gateway.push.apple.com'
feedback = connect_ssl( 'feedback.sandbox.push.apple.com' , 2196 ) #'feedback.push.apple.com'
data = json.dumps( payload, separators=(',',':'), ensure_ascii=False).encode('utf-8') #convert payload to json
if len(data) > MAX_PAYLOAD_LENGTH : raise
byteToken = a2b_hex(device_token.replace(' ','')) # Clear out spaces in the device token and convert
theNotification = struct.pack( NOTIFICATION_FORMAT % len(data), 0, 32, byteToken, len(data), data )
# $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
# send notification to cloud
gateway.write( theNotification )
# $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
# recieve response from cloud
for (token_hex, fail_time) in read_feedback() : print "FEEDBACK : { token : %s , fail_time : %s } " % (token_hex,fail_time)
# $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
# Close the connection -- apple would prefer keeping a connection open and pushing data as needed.
# Opening conection to APN takes ~ 1sec too much delay for triggers
gateway.close()
feedback.close()
# END APN DRAFT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment