Source code for autopush.router.apns2

import json
from collections import deque
from decimal import Decimal

import hyper.tls
from hyper import HTTP20Connection
from hyper.http20.exceptions import HTTP20Error

from autopush.exceptions import RouterException


SANDBOX = 'api.development.push.apple.com'
SERVER = 'api.push.apple.com'

APNS_MAX_CONNECTIONS = 20

# These values are defined by APNs as header values that should be sent.
# The hyper library requires that all header values be strings.
# These values should be considered "opaque" to APNs.
# see https://developer.apple.com/search/?q=%22apns-priority%22
APNS_PRIORITY_IMMEDIATE = '10'
APNS_PRIORITY_LOW = '5'


class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Decimal):
            return int(obj.to_integral_value())
        # for most data types, this function isn't called.
        # the following is added for safety, but should not
        # be required.
        return json.JSONEncoder.default(self, obj)  # pragma nocover


class APNSException(Exception):
    pass


[docs]class APNSClient(object):
[docs] def __init__(self, cert_file, key_file, topic, alt=False, use_sandbox=False, max_connections=APNS_MAX_CONNECTIONS, logger=None, metrics=None, load_connections=True, max_retry=2): """Create the APNS client connector. The cert_file and key_file can be derived from the exported `.p12` **Apple Push Services: *bundleID* ** key contained in the **Keychain Access** application. To extract the proper PEM formatted data, you can use the following commands: ``` openssl pkcs12 -in file.p12 -out apns_cert.pem -clcerts -nokeys openssl pkcs12 -in file.p12 -out apns_key.pem -nocerts -nodes ``` The *topic* is the Bundle ID of the bridge recipient iOS application. Since the cert needs to be tied directly to an application, the topic is usually similar to "com.example.MyApplication". :param cert_file: Path to the PEM formatted APNs certification file. :type cert_file: str :param key_file: Path to the PEM formatted APNs key file. :type key_file: str :param topic: The *Bundle ID* that identifies the assoc. iOS app. :type topic: str :param alt: Use the alternate APNs publication port (if 443 is blocked) :type alt: bool :param use_sandbox: Use the development sandbox :type use_sandbox: bool :param max_connections: Max number of pooled connections to use :type max_connections: int :param logger: Status logger :type logger: logger :param metrics: Metric recorder :type metrics: autopush.metrics.IMetric :param load_connections: used for testing :type load_connections: bool :param max_retry: Number of HTTP2 transmit attempts :type max_retry: int """ self.server = SANDBOX if use_sandbox else SERVER self.port = 2197 if alt else 443 self.log = logger self.metrics = metrics self.topic = topic self._max_connections = max_connections self._max_retry = max_retry self.connections = deque(maxlen=max_connections) if load_connections: self.ssl_context = hyper.tls.init_context(cert=(cert_file, key_file)) self.connections.extendleft((HTTP20Connection( self.server, self.port, ssl_context=self.ssl_context, force_proto='h2') for x in range(0, max_connections))) if self.log: self.log.debug("Starting APNS connection")
[docs] def send(self, router_token, payload, apns_id, priority=True, topic=None, exp=None): """Send the dict of values to the remote bridge This sends the raw data to the remote bridge application using the APNS2 HTTP2 API. :param router_token: APNs provided hex token identifying recipient :type router_token: str :param payload: Data to send to recipient :type payload: dict :param priority: True is high priority, false is low priority :type priority: bool :param topic: BundleID for the recipient application (overides default) :type topic: str :param exp: Message expiration timestamp :type exp: timestamp """ body = json.dumps(payload, cls=ComplexEncoder) priority = APNS_PRIORITY_IMMEDIATE if priority else APNS_PRIORITY_LOW # NOTE: Hyper requires that all header values be strings. 'Priority' # is a integer string, which may be "simplified" and cause an error. # The added str() function safeguards against that. headers = { 'apns-id': apns_id, 'apns-priority': str(priority), 'apns-topic': topic or self.topic, } if exp: headers['apns-expiration'] = str(exp) url = '/3/device/' + router_token attempt = 0 while True: try: connection = self._get_connection() # request auto-opens closed connections, so if a connection # has timed out or failed for other reasons, it's automatically # re-established. stream_id = connection.request( 'POST', url=url, body=body, headers=headers) # get_response() may return an AttributeError. Not really sure # how it happens, but the connected socket may get set to None. # We'll treat that as a premature socket closure. response = connection.get_response(stream_id) if response.status != 200: reason = json.loads( response.read().decode('utf-8'))['reason'] raise RouterException( "APNS Transmit Error {}:{}".format(response.status, reason), status_code=response.status, response_body="APNS could not process " "your message {}".format(reason), log_exception=True, reason=reason ) break except (HTTP20Error, IOError): connection.close() attempt += 1 if attempt < self._max_retry: continue raise finally: # Returning a closed connection to the pool is ok. # hyper will reconnect on .request() self._return_connection(connection)
def _get_connection(self): try: connection = self.connections.pop() return connection except IndexError: raise RouterException( "Too many APNS requests, increase pool from {}".format( self._max_connections ), status_code=503, response_body="APNS busy, please retry") def _return_connection(self, connection): self.connections.appendleft(connection)