Skip to content

Add HTTP/2 APNs client #216

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ frame.add_item('b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b87', paylo
apns.gateway_server.send_notification_multiple(frame)
```

## HTTP/2 sample usage

The legacy binary interface above is deprecated. The `apns2.py` module provides a
minimal client that uses Apple's modern HTTP/2 provider API.

```python
from apns2 import APNsHTTP2
from apns import Payload
import json

client = APNsHTTP2(use_sandbox=True, cert_file='cert.pem', key_file='key.pem')
payload = Payload(alert="Hello HTTP/2")
client.send_notification(
'device_token_hex', json.loads(payload.json()), topic='com.example.App'
)
```

Apple recommends to query the feedback service daily to get the list of device tokens. You need to create a new connection to APNS to see all the tokens that have failed since you only receive that information upon connection. Remember, once you have viewed the list of tokens, Apple will clear the list from their servers. Use the timestamp to verify that the device tokens haven’t been reregistered since the feedback entry was generated. For each device that has not been reregistered, stop sending notifications. By using this information to stop sending push notifications that will fail to be delivered, you reduce unnecessary message overhead and improve overall system performance.

```
Expand Down
34 changes: 34 additions & 0 deletions apns-send-http2
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python

"""Command line script for sending a push via HTTP/2"""

import optparse
import json
from apns2 import APNsHTTP2
from apns import Payload # reuse existing Payload class

parser = optparse.OptionParser()

parser.add_option("-c", "--certificate-file", dest="certificate_file", help="Path to .pem certificate file")
parser.add_option("-k", "--key-file", dest="key_file", help="Path to .pem key file")
parser.add_option("-p", "--push-token", dest="push_token", help="Push token")
parser.add_option("-t", "--topic", dest="topic", help="Topic (usually the app bundle id)")
parser.add_option("-m", "--message", dest="message", help="Message")
parser.add_option("-s", "--sandbox", action="store_true", dest="sandbox", default=False, help="Use apple sandbox")

options, args = parser.parse_args()

if not options.certificate_file or not options.key_file:
parser.error('Must provide certificate and key files')
if not options.push_token:
parser.error('Must provide --push-token')
if not options.message:
parser.error('Must provide --message')
if not options.topic:
parser.error('Must provide --topic')

apns = APNsHTTP2(use_sandbox=options.sandbox, cert_file=options.certificate_file, key_file=options.key_file)

payload = Payload(alert=options.message, sound="default", badge=1)
resp = apns.send_notification(options.push_token, json.loads(payload.json()), topic=options.topic)
print("Status:", resp.status_code)
10 changes: 5 additions & 5 deletions apns.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,17 +321,17 @@ def __init__(self, alert=None, badge=None, sound=None, category=None, custom=Non
def dict(self):
"""Returns the payload as a regular Python dictionary"""
d = {}
if self.sound:
d['sound'] = self.sound
if self.badge is not None:
d['badge'] = int(self.badge)
if self.alert:
# Alert can be either a string or a PayloadAlert
# object
if isinstance(self.alert, PayloadAlert):
d['alert'] = self.alert.dict()
else:
d['alert'] = self.alert
if self.sound:
d['sound'] = self.sound
if self.badge is not None:
d['badge'] = int(self.badge)
if self.category:
d['category'] = self.category

Expand All @@ -347,7 +347,7 @@ def dict(self):
return d

def json(self):
return json.dumps(self.dict(), separators=(',',':'), ensure_ascii=False).encode('utf-8')
return json.dumps(self.dict(), separators=(',', ':'), ensure_ascii=False).encode('utf-8')

def _check_size(self):
payload_length = len(self.json())
Expand Down
30 changes: 30 additions & 0 deletions apns2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""HTTP/2 APNs client using the modern provider API."""

import json
import httpx
from typing import Optional

class APNsHTTP2:
"""Simple APNs client that uses Apple's HTTP/2 provider API."""

def __init__(self, *, use_sandbox: bool = False, cert_file: Optional[str] = None, key_file: Optional[str] = None, timeout: float = 10.0):
self.use_sandbox = use_sandbox
self.cert_file = cert_file
self.key_file = key_file
self.timeout = timeout
self._client = httpx.Client(http2=True, verify=True, cert=(self.cert_file, self.key_file))
self.host = ("api.push.apple.com", "api.sandbox.push.apple.com")[self.use_sandbox]

def _endpoint(self, token: str) -> str:
return f"https://{self.host}/3/device/{token}"

def send_notification(self, token: str, payload: dict, topic: str, priority: int = 10) -> httpx.Response:
headers = {
"apns-topic": topic,
"apns-priority": str(priority),
}
data = json.dumps(payload).encode("utf-8")
url = self._endpoint(token)
response = self._client.post(url, headers=headers, content=data, timeout=self.timeout)
response.raise_for_status()
return response
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
download_url = 'https://github.com/djacobs/PyAPNs',
license = 'unlicense.org',
name = 'apns',
py_modules = ['apns'],
scripts = ['apns-send'],
py_modules = ['apns', 'apns2'],
scripts = ['apns-send', 'apns-send-http2'],
url = 'http://29.io/',
version = '2.0.1',
)