Created
July 23, 2014 08:50
-
-
Save jancajthaml/72395d02be5c3964ece7 to your computer and use it in GitHub Desktop.
Python APN Client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
############################################# | |
# 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