Skip to content
This repository has been archived by the owner on Feb 13, 2020. It is now read-only.

Commit

Permalink
Handle variable length APNS device tokens (#496)
Browse files Browse the repository at this point in the history
* Handle variable length APNS device tokens

* Update comment
  • Loading branch information
m0rgen committed Jun 30, 2017
1 parent 297355c commit 328e569
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 46 deletions.
51 changes: 34 additions & 17 deletions calendarserver/push/applepush.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ def sendNotification(self, token, key, dataChangedTimestamp, priority):
except:
self.log.error("Invalid APN token in database: {token}", token=token)
return
tokenLength = len(binaryToken)

identifier = self.history.add(token)
apnsPriority = ApplePushPriority.lookupByValue(priority.value).value
Expand All @@ -378,7 +379,7 @@ def sendNotification(self, token, key, dataChangedTimestamp, priority):
Top level: Command (1 byte), Frame length (4 bytes), Frame data (variable)
Within Frame data: Item ...
Item: Item number (1 byte), Item data length (2 bytes), Item data (variable)
Item 1: Device token (32 bytes)
Item 1: Device token (variable length)
Item 2: Payload (variable length) in JSON format, not null-terminated
Item 3: Notification ID (4 bytes) an opaque value used for reporting errors
Item 4: Expiration date (4 bytes) UNIX epoch in secondcs UTC
Expand All @@ -392,7 +393,7 @@ def sendNotification(self, token, key, dataChangedTimestamp, priority):
# Item 1 (Device token)
1 + # Item number # B
2 + # Item length # H
32 + # device token # 32s
tokenLength + # device token # %d s
# Item 2 (Payload)
1 + # Item number # B
2 + # Item length # H
Expand All @@ -413,13 +414,13 @@ def sendNotification(self, token, key, dataChangedTimestamp, priority):

self.transport.write(
struct.pack(
"!BIBH32sBH%dsBHIBHIBHB" % (payloadLength,),
"!BIBH%dsBH%dsBHIBHIBHB" % (tokenLength, payloadLength,),

command, # Command
frameLength, # Frame length

1, # Item 1 (Device token)
32, # Token Length
tokenLength, # Token Length
binaryToken, # Token

2, # Item 2 (Payload)
Expand Down Expand Up @@ -659,7 +660,7 @@ class APNFeedbackProtocol(Protocol):
"""
log = Logger()

MESSAGE_LENGTH = 38
PREFIX_LENGTH = 6

def connectionMade(self):
self.log.debug("FeedbackProtocol connectionMade")
Expand All @@ -668,8 +669,9 @@ def connectionMade(self):
@inlineCallbacks
def dataReceived(self, data, fn=None):
"""
Buffer and divide up received data into feedback messages which are
always 38 bytes long
Buffer and divide up received data into feedback messages. Once we've
received enough data and can read a device token, we call processFeedback( )
on it.
"""

if fn is None:
Expand All @@ -678,21 +680,36 @@ def dataReceived(self, data, fn=None):
self.log.debug("FeedbackProtocol dataReceived {len} bytes", len=len(data))
self.buffer += data

while len(self.buffer) >= self.MESSAGE_LENGTH:
message = self.buffer[:self.MESSAGE_LENGTH]
self.buffer = self.buffer[self.MESSAGE_LENGTH:]
while len(self.buffer) >= self.PREFIX_LENGTH:
prefix = self.buffer[:self.PREFIX_LENGTH]

try:
timestamp, _ignore_tokenLength, binaryToken = struct.unpack(
"!IH32s",
message)
token = binaryToken.encode("hex").lower()
yield fn(timestamp, token)
# Get the length of the token
timestamp, tokenLength = struct.unpack(
"!IH",
prefix)

messageLength = self.PREFIX_LENGTH + tokenLength

if len(self.buffer) >= messageLength:
# Now we can get the token itself
data = struct.unpack(
"!%ds" % (tokenLength,),
self.buffer[self.PREFIX_LENGTH:messageLength])
token = data[0].encode("hex").lower()
yield fn(timestamp, token)
self.buffer = self.buffer[messageLength:]

else:
# We had enough for the prefix, but not enough containing
# the token itself
return

except Exception, e:
self.log.warn(
"FeedbackProtocol could not process message: {code} ({ex})",
code=message.encode("hex"), ex=e
"FeedbackProtocol could not process message: ({ex})", ex=e
)
return

@inlineCallbacks
def processFeedback(self, timestamp, token):
Expand Down
53 changes: 26 additions & 27 deletions calendarserver/push/test/test_applepush.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ def test_ApplePushNotifierService(self):
except InvalidSubscriptionValues:
pass

token = "2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"
token2 = "3d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"
# Specifically not using 32-byte (64 hex char) in length tokens here, as
# device tokens are not fixed length:
token = "2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219dfaa"
token2 = "3d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219dfaa"
key1 = "/CalDAV/calendars.example.com/user01/calendar/"
timestamp1 = 1000
uid = "D2256BCC-48E2-42D1-BD89-CBA1E4CCDFFB"
Expand Down Expand Up @@ -157,36 +159,37 @@ def callWhenRunning(callable, *args):
# Verify data sent to APN
providerConnector = service.providers["CalDAV"].testConnector
rawData = providerConnector.transport.data
self.assertEquals(len(rawData), 199)
self.assertEquals(len(rawData), 200)
data = struct.unpack("!BI", rawData[:5])
self.assertEquals(data[0], 2) # command
self.assertEquals(data[1], 194) # frame length
self.assertEquals(data[1], 195) # frame length
# Item 1 (device token)
data = struct.unpack("!BH32s", rawData[5:40])
self.assertEquals(data[0], 1)
self.assertEquals(data[1], 32)
self.assertEquals(data[2].encode("hex"), token.replace(" ", "")) # token
itemNum, tokenLength = struct.unpack("!BH", rawData[5:8])
self.assertEquals(itemNum, 1)
self.assertEquals(tokenLength, len(token) / 2)
data = struct.unpack("!%ds" % (tokenLength,), rawData[8:8 + tokenLength])
self.assertEquals(data[0].encode("hex"), token.replace(" ", "")) # token
# Item 2 (payload)
data = struct.unpack("!BH", rawData[40:43])
data = struct.unpack("!BH", rawData[41:44])
self.assertEquals(data[0], 2)
payloadLength = data[1]
self.assertEquals(payloadLength, 138)
payload = struct.unpack("!%ds" % (payloadLength,), rawData[43:181])
payload = struct.unpack("!%ds" % (payloadLength,), rawData[44:182])
payload = json.loads(payload[0])
self.assertEquals(payload["key"], u"/CalDAV/calendars.example.com/user01/calendar/")
self.assertEquals(payload["dataChangedTimestamp"], dataChangedTimestamp)
self.assertTrue("pushRequestSubmittedTimestamp" in payload)
# Item 3 (notification id)
data = struct.unpack("!BHI", rawData[181:188])
data = struct.unpack("!BHI", rawData[182:189])
self.assertEquals(data[0], 3)
self.assertEquals(data[1], 4)
self.assertEquals(data[2], 2)
# Item 4 (expiration)
data = struct.unpack("!BHI", rawData[188:195])
data = struct.unpack("!BHI", rawData[189:196])
self.assertEquals(data[0], 4)
self.assertEquals(data[1], 4)
# Item 5 (priority)
data = struct.unpack("!BHB", rawData[195:199])
data = struct.unpack("!BHB", rawData[196:200])
self.assertEquals(data[0], 5)
self.assertEquals(data[1], 1)
self.assertEquals(data[2], ApplePushPriority.high.value)
Expand All @@ -208,17 +211,17 @@ def callWhenRunning(callable, *args):
priority=PushPriority.low)
yield txn.commit()
clock.advance(1) # so that first push is sent
self.assertEquals(len(providerConnector.transport.data), 199)
self.assertEquals(len(providerConnector.transport.data), 200)
# Ensure that the priority is "low"
data = struct.unpack("!BHB", providerConnector.transport.data[195:199])
data = struct.unpack("!BHB", providerConnector.transport.data[196:200])
self.assertEquals(data[0], 5)
self.assertEquals(data[1], 1)
self.assertEquals(data[2], ApplePushPriority.low.value)

# Reset sent data
providerConnector.transport.data = None
clock.advance(3) # so that second push is sent
self.assertEquals(len(providerConnector.transport.data), 199)
self.assertEquals(len(providerConnector.transport.data), 200)

history = []

Expand Down Expand Up @@ -262,9 +265,10 @@ def errorTestFunction(status, identifier):
feedbackConnector = service.feedbacks["CalDAV"].testConnector
timestamp = 2000
binaryToken = token.decode("hex")
tokenLength = len(binaryToken)
feedbackData = struct.pack(
"!IH32s", timestamp, len(binaryToken),
binaryToken)
"!IH%ds" % (tokenLength,), timestamp, tokenLength, binaryToken
)
yield feedbackConnector.receiveData(feedbackData)

# Simulate feedback with multiple tokens, and dataReceived called
Expand All @@ -277,9 +281,9 @@ def feedbackTestFunction(timestamp, token):
timestamp = 2000
binaryToken = token.decode("hex")
feedbackData = struct.pack(
"!IH32sIH32s",
timestamp, len(binaryToken), binaryToken,
timestamp, len(binaryToken), binaryToken,
"!IH%dsIH%ds" % (tokenLength, tokenLength),
timestamp, tokenLength, binaryToken,
timestamp, tokenLength, binaryToken,
)
# Send 1st 10 bytes
yield feedbackConnector.receiveData(feedbackData[:10], fn=feedbackTestFunction)
Expand All @@ -289,11 +293,6 @@ def feedbackTestFunction(timestamp, token):
# Buffer is empty
self.assertEquals(len(feedbackConnector.service.protocol.buffer), 0)

# Sending 39 bytes
yield feedbackConnector.receiveData("!" * 39, fn=feedbackTestFunction)
# Buffer has 1 byte remaining
self.assertEquals(len(feedbackConnector.service.protocol.buffer), 1)

# The second subscription should now be gone
txn = self._sqlCalendarStore.newTransaction()
subscriptions = (yield txn.apnSubscriptionsByToken(token))
Expand Down Expand Up @@ -346,7 +345,7 @@ def feedbackTestFunction(timestamp, token):

def test_validToken(self):
self.assertTrue(validToken("2d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"))
self.assertFalse(validToken("d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219df"))
self.assertTrue(validToken("d0d55cd7f98bcb81c6e24abcdc35168254c7846a43e2828b1ba5a8f82e219d"))
self.assertFalse(validToken("foo"))
self.assertFalse(validToken(""))

Expand Down
4 changes: 2 additions & 2 deletions calendarserver/push/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ def getAPNTopicFromX509(x509):

def validToken(token):
"""
Return True if token is in hex and is 64 characters long, False
Return True if token is in hex and is not empty, False
otherwise
"""
if len(token) != 64:
if not token:
return False

try:
Expand Down

0 comments on commit 328e569

Please sign in to comment.