Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token Feedback (protocol version2) + Blacklist xmlrpc methods #31

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ These methods can be called on the server you started the server on. Be sure you
Array(Array(Datetime(time_expired), String(token)), ...)


### blacklist

Arguments
app_id String the application id to retrieve
retrieve blacklisted
(invalid) tokens for

Returns
Array(String(token), ...)


### The Python API
pyapns also provides a Python API that makes the use of pyapns even simpler. The Python API must be configured before use but configuration files make it easier. The pyapns `client` module currently supports configuration from Django settings and Pylons config. To configure using Django, the following must be present in your settings file:

Expand Down Expand Up @@ -193,6 +204,21 @@ Each of these functions can be called synchronously and asynchronously. To make
Returns:
List of feedback tuples like [(datetime_expired, token_str), ...]

### `pyapns.client.blacklist(app_id, async=False, callback=None, errback=None)`

Retrieves a list of blacklisted (invalid) tokens from the pyapns server.

Arguments:
app_id the app_id to query
async pass something truthy to execute the request in
a background thread
callback a function to be executed with the result
errback a function to be executed with the error if there
is one during the request

Returns:
List of blacklisted token strings like [String(token), ...]


## The Ruby API

Expand Down
2 changes: 1 addition & 1 deletion pyapns/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .client import notify, provision, feedback, configure__version__ = "0.4.0"__author__ = "Samuel Sutch"__license__ = "MIT"__copyright__ = "Copyrighit 2012 Samuel Sutch"
from .client import notify, provision, feedback, blacklist, configure__version__ = "0.4.0"__author__ = "Samuel Sutch"__license__ = "MIT"__copyright__ = "Copyrighit 2012 Samuel Sutch"
Expand Down
12 changes: 12 additions & 0 deletions pyapns/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ def feedback(app_id, async=False, callback=None, errback=None):
t.daemon = True
t.start()


@default_callback
@reprovision_and_retry
def blacklist(app_id, async=False, callback=None, errback=None):
args = [app_id]
f_args = ['blacklist', args, callback, errback]
if not async:
return _xmlrpc_thread(*f_args)
t = threading.Thread(target=_xmlrpc_thread, args=f_args)
t.daemon = True
t.start()

def _xmlrpc_thread(method, args, callback, errback=None):
if not configure({}):
raise APNSNotConfigured('APNS Has not been configured.')
Expand Down
94 changes: 78 additions & 16 deletions pyapns/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import struct
import binascii
import datetime
import time
from StringIO import StringIO as _StringIO
from OpenSSL import SSL, crypto
from twisted.internet import reactor, defer
Expand All @@ -22,8 +23,12 @@
FEEDBACK_SERVER_SANDBOX_HOSTNAME = "feedback.sandbox.push.apple.com"
FEEDBACK_SERVER_HOSTNAME = "feedback.push.apple.com"
FEEDBACK_SERVER_PORT = 2196
MAX_TOKEN_ID = 1024

app_ids = {} # {'app_id': APNSService()}
token_id = 0
current_tokens = {} # dict to hold current token ids
app_ids = {} # {'app_id': APNSService()}
bad_tokens = {} # {'app_id': []}

class StringIO(_StringIO):
"""Add context management protocol to StringIO
Expand Down Expand Up @@ -69,25 +74,33 @@ def getContext(self):


class APNSProtocol(Protocol):
def __init__(self):
self.appId = None
def connectionMade(self):
log.msg('APNSProtocol connectionMade')
self.factory.addClient(self)

def sendMessage(self, msg):
log.msg('APNSProtocol sendMessage msg=%s' % binascii.hexlify(msg))
return self.transport.write(msg)
self.appId = msg['appid']
log.msg('APNSProtocol sendMessage msg=%s' % binascii.hexlify(msg['msg']))
return self.transport.write(msg['msg'])

def connectionLost(self, reason):
log.msg('APNSProtocol connectionLost')
self.factory.removeClient(self)

def dataReceived(self, data):
fmt = '!BBI'
message = struct.unpack(fmt, data)
self.factory.addToBlacklist( self.appId, current_tokens[message[2]], message[1])


class APNSFeedbackHandler(LineReceiver):
MAX_LENGTH = 1024*1024

def connectionMade(self):
log.msg('feedbackHandler connectionMade')

def rawDataReceived(self, data):
log.msg('feedbackHandler rawDataReceived %s' % binascii.hexlify(data))
self.io.write(data)
Expand Down Expand Up @@ -148,6 +161,13 @@ def removeClient(self, p):
def startedConnecting(self, connector):
log.msg('APNSClientFactory startedConnecting')

def addToBlacklist(self, appId, token, reason):
log.msg('WARNING - blacklisting AppId: %s token: %s reason: %s' % (appId, token, reason))
global bad_tokens
if not bad_tokens.get(appId, None):
bad_tokens[appId] = []
bad_tokens[appId].append(token)

def buildProtocol(self, addr):
self.resetDelay()
p = self.protocol()
Expand Down Expand Up @@ -273,15 +293,16 @@ def xmlrpc_provision(self, app_id, path_to_cert_or_cert, environment, timeout=15
# log.msg('provisioning ' + app_id + ' environment ' + environment)
self.app_ids[app_id] = APNSService(path_to_cert_or_cert, environment, timeout)

def xmlrpc_notify(self, app_id, token_or_token_list, aps_dict_or_list):
def xmlrpc_notify(self, app_id, token_or_token_list, aps_dict_or_list, expiry_or_expiry_list=None):
""" Sends push notifications to the Apple APNS server. Multiple
notifications can be sent by sending pairing the token/notification
arguments in lists [token1, token2], [notification1, notification2].

Arguments:
app_id provisioned app_id to send to
token_or_token_list token to send the notification or a list of tokens
aps_dict_or_list notification dicts or a list of notifications
aps_dict_or_list notification dicts or a list of notifications
expiry_or_expiry_list epoch expiry timestamp or list of timestamps, default now
Returns:
None
"""
Expand All @@ -290,7 +311,7 @@ def xmlrpc_notify(self, app_id, token_or_token_list, aps_dict_or_list):
[t.replace(' ', '') for t in token_or_token_list]
if (type(token_or_token_list) is list)
else token_or_token_list.replace(' ', ''),
aps_dict_or_list))
aps_dict_or_list, expiry_or_expiry_list, app_id))
if d:
def _finish_err(r):
# so far, the only error that could really become of this
Expand All @@ -312,24 +333,65 @@ def xmlrpc_feedback(self, app_id):

return self.apns_service(app_id).read().addCallback(
lambda r: decode_feedback(r))

def xmlrpc_blacklist(self, app_id):
""" Queries blacklisted (invalid) tokens. Returns a list of token Strings. The List in pyapns will be cleared.

Arguments:
app_id the app_id to query
Returns:
Blacklisted token Strings as List
"""

if bad_tokens.get(app_id, None):
return bad_tokens.pop(app_id)
else:
return []


def encode_notifications(tokens, notifications):
def encode_notifications(tokens, notifications, expirys, appId):
""" Returns the encoded bytes of tokens and notifications

tokens a list of tokens or a string of only one token
notifications a list of notifications or a dictionary of only one
notifications a list of notifications or a dictionary of only one
timeouts a list of timeout values
"""

fmt = "!BH32sH%ds"
structify = lambda t, p: struct.pack(fmt % len(p), 0, 32, t, len(p), p)
fmt = "!BIIH32sH%ds"
structify = lambda t, p, e, ti: struct.pack(fmt % len(p), 1, ti, e, 32, t, len(p), p)
binaryify = lambda t: t.decode('hex')
if type(notifications) is dict and type(tokens) in (str, unicode):
tokens, notifications = ([tokens], [notifications])
if type(notifications) is list and type(tokens) is list:
return ''.join(map(lambda y: structify(*y), ((binaryify(t), json.dumps(p, separators=(',',':')))
for t, p in zip(tokens, notifications))))

if expirys is None:
expirys = time.time()
expirys = [expirys]*len(notifications)
log.msg('expirys: %s' % expirys)
log.msg('tokens: %s' % tokens)
log.msg('notifications: %s' % notifications)
if type(notifications) is list and type(tokens) is list and type(expirys) is list:
ids = []
global token_id
global MAX_TOKEN_ID
global bad_tokens
for t in tokens:
if bad_tokens.get(appId, None):
if t in bad_tokens[appId]:
log.msg('WARNING - ignoring bad token: %s for appId: %s' % (t, appId))
index = tokens.index(t)
tokens.pop(index)
notifications.pop(index)
expirys.pop(index)
break
if token_id == MAX_TOKEN_ID:
token_id = 0
token_id += 1
current_tokens[token_id] = t
ids.append(token_id)
messages = zip(tokens, notifications, expirys, ids)
log.msg('messages: %s' % messages)
return {'appid': appId,
'msg': ''.join(map(lambda y: structify(*y), ((binaryify(t), json.dumps(n, separators=(',',':')), int(e), i)
for t, n, e, i in messages)))}

def decode_feedback(binary_tuples):
""" Returns a list of tuples in (datetime, token_str) format

Expand Down
25 changes: 22 additions & 3 deletions ruby-client/pyapns/lib/pyapns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ module PYAPNS
## PYAPNS::Client
## There's python in my ruby!
##
## This is a class used to send notifications, provision applications and
## retrieve feedback using the Apple Push Notification Service.
## This is a class used to send notifications, provision applications query invalid tokens
## and retrieve feedback using the Apple Push Notification Service.
##
## PYAPNS is a multi-application APS provider, meaning it is possible to send
## notifications to any number of different applications from the same application
Expand Down Expand Up @@ -107,12 +107,27 @@ module PYAPNS
## searching for or comparing the token received in the hexadecimal form returned,
## note that it's lowercase hex.
##
## Retrieving Blacklist
##
## The pyapns maintains a blacklist that stores invalid tokens (i.e. sandbox
## tokens in production) Such invalid tokens cause a drop of the APNS connection.
## PYAPNS will detect the invalid tokens causing a connection drop and add them
## to a blacklist on an Application basis. The blacklist is used to filter out
## invalid tokens in order to prevent connection drops
## PYAPNS will return an Array of tokens:
##
## blacklist = client.blacklist 'cf'
## => ['token', ... ]
##
## Note that once you query the blacklist, it will be removed so you need to ensure
## that this tokens get de-provisioned from your push token list
##
## Asynchronous Calls
##
## PYAPNS::Client will, by default, perform no funny stuff and operate entirely
## within the calling thread. This means that certain applications may hang when,
## say, sending a notification, if only for a fraction of a second. Obviously
## not a desirable trait, all `provision`, `feedback` and `notify`
## not a desirable trait, all `provision`, `feedback`, `blacklist` and `notify`
## methods also take a block, which indicates to the method you want to call
## PYAPNS asynchronously, and it will be done so handily in another thread, calling
## back your block with a single argument when finished. Note that `notify` and `provision`
Expand Down Expand Up @@ -159,6 +174,10 @@ def notify(*args, &block)
def feedback(*args, &block)
perform_call :feedback, args, :app_id, &block
end

def blacklist(*args, &block)
perform_call :blacklist, args, :app_id, &block
end

def perform_call(method, splat, *args, &block)
if !configured?
Expand Down