From f5d79b6d1632b840a0f92ca0d15e6eb9093bf094 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Fri, 16 Sep 2016 16:24:10 -0700 Subject: [PATCH] feat: Add multiple cert handlers for APNs This patch updates APNs handlers to accept platform based cert configurations. See `configs/autopush_shared.ini.sample`. In addition, this patch clarifies some argument references for routers (e.g. less than useful `result` is now slightly more descriptive `uaid_data`) Custom item names have been normalized to match gcm/fcm. Document errors cleaned up a bit as well. BREAKING CHANGE: the APNS configuration options have been altered, see `configs/autopush_shared.ini.sample` for new APNS configuration settings. Closes #655 --- autopush/endpoint.py | 21 ++-- autopush/main.py | 40 +++--- autopush/router/apnsrouter.py | 175 ++++++++++++++++++-------- autopush/router/fcm.py | 7 +- autopush/router/gcm.py | 11 +- autopush/router/interface.py | 9 +- autopush/tests/test_endpoint.py | 2 +- autopush/tests/test_main.py | 16 ++- autopush/tests/test_router.py | 179 +++++++++++++++++++-------- configs/autopush_endpoint.ini.sample | 7 -- configs/autopush_shared.ini.sample | 14 +++ docs/api.rst | 1 - docs/api/router/simple.rst | 5 - docs/api/senderids.rst | 15 --- docs/http.rst | 5 +- 15 files changed, 332 insertions(+), 175 deletions(-) delete mode 100644 docs/api/senderids.rst diff --git a/autopush/endpoint.py b/autopush/endpoint.py index 04778fae..ab98e370 100644 --- a/autopush/endpoint.py +++ b/autopush/endpoint.py @@ -494,10 +494,11 @@ def _token_valid(self, result): d.addErrback(self._uaid_not_found_err) self._db_error_handling(d) - def _uaid_lookup_results(self, result): + def _uaid_lookup_results(self, uaid_data): """Process the result of the AWS UAID lookup""" # Save the whole record - router_key = self.router_key = result.get("router_type", "simplepush") + router_key = self.router_key = uaid_data.get("router_type", + "simplepush") self._client_info["router_key"] = router_key try: @@ -563,7 +564,7 @@ def _uaid_lookup_results(self, result): return if use_simplepush: - self._route_notification(self.version, result, data) + self._route_notification(self.version, uaid_data, data) return # Web Push and bridged messages are encrypted binary blobs. We store @@ -573,10 +574,10 @@ def _uaid_lookup_results(self, result): # Generate a message ID, then route the notification. d = deferToThread(self.ap_settings.fernet.encrypt, ':'.join([ 'm', self.uaid, self.chid]).encode('utf8')) - d.addCallback(self._route_notification, result, data, ttl) + d.addCallback(self._route_notification, uaid_data, data, ttl) return d - def _route_notification(self, version, result, data, ttl=None): + def _route_notification(self, version, uaid_data, data, ttl=None): self.version = self._client_info['message_id'] = version warning = "" # Clean up the header values (remove padding) @@ -592,8 +593,8 @@ def _route_notification(self, version, result, data, ttl=None): ttl=ttl) d = Deferred() - d.addCallback(self.router.route_notification, result) - d.addCallback(self._router_completed, result, warning) + d.addCallback(self.router.route_notification, uaid_data) + d.addCallback(self._router_completed, uaid_data, warning) d.addErrback(self._router_fail_err) d.addErrback(self._response_err) @@ -704,7 +705,7 @@ def post(self, router_type="", router_token="", uaid="", chid=""): if new_uaid: d = Deferred() d.addCallback(router.register, router_data=params, - reg_id=router_token, uri=self.request.uri) + app_id=router_token, uri=self.request.uri) d.addCallback(self._save_router_data, router_type) d.addCallback(self._create_endpoint) d.addCallback(self._return_endpoint, new_uaid, router) @@ -741,8 +742,8 @@ def put(self, router_type="", router_token="", uaid="", chid=""): self.add_header("Content-Type", "application/json") d = Deferred() - d.addCallback(router.register, reg_id=router_token, - router_data=router_data, uri=self.request.uri) + d.addCallback(router.register, router_data=router_data, + app_id=router_token, uri=self.request.uri) d.addCallback(self._save_router_data, router_type) d.addCallback(self._success) d.addErrback(self._router_fail_err) diff --git a/autopush/main.py b/autopush/main.py index b2c5eebc..1ce1c309 100644 --- a/autopush/main.py +++ b/autopush/main.py @@ -146,6 +146,11 @@ def obsolete_args(parser): parser.add_argument('--max_message_size', type=int, help="OBSOLETE") parser.add_argument('--s3_bucket', help='OBSOLETE') parser.add_argument('--senderid_expry', help='OBSOLETE') + # old APNs args + parser.add_argument('--apns_enabled', help="OBSOLETE") + parser.add_argument('--apns_sandbox', help="OBSOLETE") + parser.add_argument('--apns_cert_file', help="OBSOLETE") + parser.add_argument('--apns_key_file', help="OBSOLETE") def add_external_router_args(parser): @@ -187,18 +192,14 @@ def add_external_router_args(parser): parser.add_argument('--fcm_senderid', help='SenderID for FCM', type=str, default="") # Apple Push Notification system (APNs) for iOS - parser.add_argument('--apns_enabled', help="Enable APNS Bridge", - action="store_true", default=False, - env_var="APNS_ENABLED") - label = "APNS Router:" - parser.add_argument('--apns_sandbox', help="%s Use Dev Sandbox" % label, - action="store_true", default=False, - env_var="APNS_SANDBOX") - parser.add_argument('--apns_cert_file', - help="%s Certificate PEM file" % label, - type=str, env_var="APNS_CERT_FILE") - parser.add_argument('--apns_key_file', help="%s Key PEM file" % label, - type=str, env_var="APNS_KEY_FILE") + # credentials consist of JSON struct containing a channel type + # followed by the settings, + # e.g. {'firefox':{'cert': 'path.cert', 'key': 'path.key', + # 'sandbox': false}, ... } + parser.add_argument('--apns_creds', help="JSON dictionary of " + "APNS settings", + type=str, default="", + env_var="APNS_CREDS") # UDP parser.add_argument('--wake_timeout', help="UDP: idle timeout before closing socket", @@ -313,12 +314,14 @@ def make_settings(args, **kwargs): router_conf["simplepush"] = {"idle": args.wake_timeout, "server": args.wake_server, "cert": args.wake_pem} - if args.apns_enabled: + if args.apns_creds: # if you have the critical elements for each external router, create it - if args.apns_cert_file is not None and args.apns_key_file is not None: - router_conf["apns"] = {"sandbox": args.apns_sandbox, - "cert_file": args.apns_cert_file, - "key_file": args.apns_key_file} + try: + router_conf["apns"] = json.loads(args.apns_creds) + except (ValueError, TypeError): + log.critical(format="Invalid JSON specified for APNS config " + "options") + return if args.gcm_enabled: # Create a common gcmclient try: @@ -566,6 +569,9 @@ def endpoint_main(sysargs=None, use_files=True): # Start the table rotation checker/updater l = task.LoopingCall(settings.update_rotating_tables) l.start(60) + if settings.routers.get('apns'): + l = task.LoopingCall(settings.routers['apns']._cleanup) + l.start(10) reactor.suggestThreadPoolSize(50) reactor.run() diff --git a/autopush/router/apnsrouter.py b/autopush/router/apnsrouter.py index 484b6a74..62e3c325 100644 --- a/autopush/router/apnsrouter.py +++ b/autopush/router/apnsrouter.py @@ -1,10 +1,10 @@ """APNS Router""" import time +import uuid import apns from twisted.logger import Logger from twisted.internet.threads import deferToThread - from autopush.router.interface import RouterException, RouterResponse @@ -27,86 +27,163 @@ class APNSRouter(object): 255: 'Unknown', } - def _connect(self): - """Connect to APNS""" - self.apns = apns.APNs(use_sandbox=self.config.get("sandbox", False), - cert_file=self.config.get("cert_file"), - key_file=self.config.get("key_file"), - enhanced=True) + def _connect(self, cert_info): + """Connect to APNS + + :param cert_info: APNS certificate configuration info + :type cert_info: dict + + returns an instance of APNs that can be stored under the proper + release channel name. + + """ + # Do I still need to call this in _error? + return apns.APNs( + use_sandbox=cert_info.get("sandbox", False), + cert_file=cert_info.get("cert"), + key_file=cert_info.get("key"), + enhanced=True) def __init__(self, ap_settings, router_conf): """Create a new APNS router and connect to APNS""" self.ap_settings = ap_settings self._base_tags = [] - self.config = router_conf - self.default_title = router_conf.get("default_title", "SimplePush") - self.default_body = router_conf.get("default_body", "New Alert") - self._connect() - self.log.debug("Starting APNS router...") + self.apns = dict() + self.messages = dict() + self._config = router_conf + self._max_messages = self._config.pop('max_messages', 100) + for rel_channel in self._config: + self.apns[rel_channel] = self._connect(self._config[rel_channel]) + self.apns[rel_channel].gateway_server.register_response_listener( + self._error) self.ap_settings = ap_settings + self.log.debug("Starting APNS router...") - def register(self, uaid, router_data, *args, **kwargs): - """Validate that an APNs instance token is in the ``router_data``""" + def register(self, uaid, router_data, app_id, *args, **kwargs): + """Register an endpoint for APNS, on the `app_id` release channel. + + This will validate that an APNs instance token is in the + ``router_data``, + + :param uaid: User Agent Identifier + :type uaid: str + :param router_data: Dict containing router specific configuration info + :type router_data: dict + :param app_id: The release channel identifier for cert info lookup + :type app_id: str + + returns a modified router_data dict to be stored + in the user agent record. + + """ + if app_id not in self.apns: + raise RouterException("Unknown release channel specified", + status_code=400, + response_body="Unknown release channel") if not router_data.get("token"): raise RouterException("No token registered", status_code=500, response_body="No token registered") + router_data["rel_channel"] = app_id return router_data def amend_msg(self, msg, router_data=None): + """This function is stubbed out for this router""" return msg def route_notification(self, notification, uaid_data): - """Start the APNS notification routing, returns a deferred""" + """Start the APNS notification routing, returns a deferred + + :param notification: Notification data to send + :type notification: dict + :param uaid_data: User Agent specific data + :type uaid_data: dict + + """ router_data = uaid_data["router_data"] # Kick the entire notification routing off to a thread - return deferToThread(self._route, notification, router_data) + d = deferToThread(self._route, notification, router_data) + return d def _route(self, notification, router_data): - """Blocking APNS call to route the notification""" - token = router_data["token"] + """Blocking APNS call to route the notification + + :param notification: Notification data to send + :type notification: dict + :param router_data: Pre-initialized data for this connection + :type router_data: dict + + """ + router_token = router_data["token"] + rel_channel = router_data["rel_channel"] + config = self._config[rel_channel] + if len(self.messages) >= self._max_messages: + raise RouterException("Too many messages in pending queue", + status_code=503, + response_body="Pending buffer full", + ) + apns_client = self.apns[rel_channel] custom = { - "Chid": notification.channel_id, - "Ver": notification.version, + "chid": notification.channel_id, + "ver": notification.version, } if notification.data: - custom["Msg"] = notification.data - custom["Con"] = notification.headers["content-encoding"] - custom["Enc"] = notification.headers["encryption"] + custom["body"] = notification.data + custom["con"] = notification.headers["content-encoding"] + custom["enc"] = notification.headers["encryption"] if "crypto-key" in notification.headers: - custom["Cryptokey"] = notification.headers["crypto-key"] + custom["cryptokey"] = notification.headers["crypto-key"] elif "encryption-key" in notification.headers: - custom["Enckey"] = notification.headers["encryption-key"] - - payload = apns.Payload(alert=router_data.get("title", - self.default_title), - content_available=1, - custom=custom) - now = int(time.time()) - self.messages[now] = {"token": token, "payload": payload} - # TODO: Add listener for error handling. - self.apns.gateway_server.register_response_listener(self._error) - self.ap_settings.metrics.increment( - "updates.client.bridge.apns.attempted", - self._base_tags) + custom["enckey"] = notification.headers["encryption-key"] - self.apns.gateway_server.send_notification(token, payload, now) + payload = apns.Payload( + alert=router_data.get("title", config.get('default_title', + 'Mozilla Push')), + content_available=1, + custom=custom) + now = time.time() - # cleanup sent messages - if self.messages: - for time_sent in self.messages.keys(): - if time_sent < now - self.config.get("expry", 10): - del self.messages[time_sent] + # "apns-id" + msg_id = str(uuid.uuid4()) + self.messages[msg_id] = { + "time_sent": now, + "rel_channel": router_data["rel_channel"], + "router_token": router_token, + "payload": payload} self.ap_settings.metrics.increment( - "updates.client.bridge.apns.succeed", + "updates.client.bridge.apns.%s.attempted" % + router_data["rel_channel"], self._base_tags) + + apns_client.gateway_server.send_notification(router_token, payload, + msg_id) location = "%s/m/%s" % (self.ap_settings.endpoint_url, notification.version) + self.ap_settings.metrics.increment( + "updates.client.bridge.apns.%s.succeed" % + router_data["rel_channel"], + self._base_tags) return RouterResponse(status_code=201, response_body="", headers={"TTL": notification.ttl, "Location": location}, logged_status=200) + def _cleanup(self): + """clean up pending, but expired messages. + + APNs may not always respond with a status code, this will clean out + pending retryable messages. + + """ + for msg_id in self.messages.keys(): + message = self.messages[msg_id] + expry = self._config[message['rel_channel']].get("expry", 10) + if message["time_sent"] < time.time() - expry: + try: + del self.messages[msg_id] + except KeyError: # pragma nocover + pass + def _error(self, err): """Error handler""" if err['status'] == 0: @@ -117,11 +194,11 @@ def _error(self, err): status=self.errors[err['status']]) if err['status'] in [1, 255]: self.log.debug("Retrying...") - self._connect() resend = self.messages.get(err.get('identifier')) if resend is None: return - self.apns.gateway_server.send_notification(resend['token'], - resend['payload'], - err['identifier'], - ) + apns_client = self.apns[resend["rel_channel"]] + apns_client.gateway_server.send_notification(resend['token'], + resend['payload'], + err['identifier'], + ) diff --git a/autopush/router/fcm.py b/autopush/router/fcm.py index e109ca01..9690003f 100644 --- a/autopush/router/fcm.py +++ b/autopush/router/fcm.py @@ -120,21 +120,22 @@ def amend_msg(self, msg, data=None): msg["senderid"] = data.get('creds', {}).get('senderID') return msg - def register(self, uaid, router_data, reg_id=None, *args, **kwargs): + def register(self, uaid, router_data, app_id=None, *args, **kwargs): """Validate that the FCM Instance Token is in the ``router_data``""" + senderid = app_id # "token" is the GCM registration id token generated by the client. if "token" not in router_data: raise self._error("connect info missing FCM Instance 'token'", status=401, uri=kwargs.get('uri'), - senderid=repr(reg_id)) + senderid=repr(senderid)) # senderid is the remote client's senderID value. This value is # very difficult for the client to change, and there was a problem # where some clients had an older, invalid senderID. We need to # be able to match senderID to it's corresponding auth key. # If the client has an unexpected or invalid SenderID, # it is impossible for us to reach them. - if not (reg_id == self.senderID): + if not (senderid == self.senderID): raise self._error("Invalid SenderID", status=410, errno=105) # Assign a senderid router_data["creds"] = {"senderID": self.senderID, diff --git a/autopush/router/gcm.py b/autopush/router/gcm.py index 187e0b2c..b50df4ad 100644 --- a/autopush/router/gcm.py +++ b/autopush/router/gcm.py @@ -44,7 +44,7 @@ def amend_msg(self, msg, data=None): msg["senderid"] = data.get('creds', {}).get('senderID') return msg - def register(self, uaid, router_data, reg_id=None, *args, **kwargs): + def register(self, uaid, router_data, app_id, *args, **kwargs): """Validate that the GCM Instance Token is in the ``router_data``""" # "token" is the GCM registration id token generated by the client. if "token" not in router_data: @@ -56,13 +56,14 @@ def register(self, uaid, router_data, reg_id=None, *args, **kwargs): # be able to match senderID to it's corresponding auth key. # If the client has an unexpected or invalid SenderID, # it is impossible for us to reach them. - if reg_id not in self.senderIDs: + senderid = app_id + if senderid not in self.senderIDs: raise self._error("Invalid SenderID", status=410, errno=105, uri=kwargs.get('uri'), - senderid=repr(reg_id)) + senderid=senderid) # Assign a senderid - router_data["creds"] = {"senderID": reg_id, - "auth": self.senderIDs[reg_id]} + router_data["creds"] = {"senderID": senderid, + "auth": self.senderIDs[senderid]} return router_data def route_notification(self, notification, uaid_data): diff --git a/autopush/router/interface.py b/autopush/router/interface.py index 1332c078..0f9fea4c 100644 --- a/autopush/router/interface.py +++ b/autopush/router/interface.py @@ -46,11 +46,18 @@ def __init__(self, settings, router_conf): the given settings and router conf.""" raise NotImplementedError("__init__ must be implemented") - def register(self, uaid, router_data, *args, **kwargs): + def register(self, uaid, routing_data, app_id, *args, **kwargs): """Register the uaid with the connect dict however is preferred and return a dict that will be stored as routing_data for this user in the future. + :param uaid: User Agent Identifier + :type uaid: str + :param routing_data: Route specific configuration info + :type routing_data: dict + :param app_id: Application identifier from URI + :type app_id: str + :returns: A response object :rtype: :class:`RouterResponse` :raises: diff --git a/autopush/tests/test_endpoint.py b/autopush/tests/test_endpoint.py index d29a869e..d6f294c9 100644 --- a/autopush/tests/test_endpoint.py +++ b/autopush/tests/test_endpoint.py @@ -1880,8 +1880,8 @@ def handle_finish(value): self.reg.write.assert_called_with({}) frouter.register.assert_called_with( dummy_uaid, - reg_id='', router_data=data, + app_id='', uri=self.reg.request.uri ) diff --git a/autopush/tests/test_main.py b/autopush/tests/test_main.py index ce50e716..11090fe2 100644 --- a/autopush/tests/test_main.py +++ b/autopush/tests/test_main.py @@ -1,5 +1,6 @@ import unittest import datetime +import json from mock import Mock, patch from moto import mock_dynamodb2 @@ -203,12 +204,10 @@ def test_skip_logging(self): class EndpointMainTestCase(unittest.TestCase): class TestArg: # important stuff - apns_enabled = True - apns_cert_file = "cert.file" - apns_key_file = "key.file" + apns_creds = json.dumps({"firefox": {"cert": "cert.file", + "key": "key.file"}}) gcm_enabled = True # less important stuff - apns_sandbox = False gcm_ttl = 999 gcm_dryrun = False gcm_collapsekey = "collapse" @@ -278,14 +277,19 @@ def test_bad_senderidlist(self): "--senderid_list='[Invalid'" ], False) + def test_bad_apnsconf(self): + assert endpoint_main([ + "--apns_creds='[Invalid'" + ], False) + def test_ping_settings(self): ap = make_settings(self.TestArg) # verify that the hostname is what we said. eq_(ap.hostname, self.TestArg.hostname) # gcm isn't created until later since we may have to pull # config info from s3 - eq_(ap.routers["apns"].apns.cert_file, self.TestArg.apns_cert_file) - eq_(ap.routers["apns"].apns.key_file, self.TestArg.apns_key_file) + eq_(ap.routers["apns"].apns["firefox"].cert_file, "cert.file") + eq_(ap.routers["apns"].apns["firefox"].key_file, "key.file") eq_(ap.wake_timeout, 10) def test_bad_senders(self): diff --git a/autopush/tests/test_router.py b/autopush/tests/test_router.py index 76482750..bddf34b4 100644 --- a/autopush/tests/test_router.py +++ b/autopush/tests/test_router.py @@ -8,6 +8,7 @@ from nose.tools import eq_, ok_ from twisted.trial import unittest from twisted.internet.error import ConnectError, ConnectionRefusedError +from twisted.python.failure import Failure import apns import gcmclient @@ -68,7 +69,7 @@ def init(self, settings, router_conf): pass IRouter.__init__ = init ir = IRouter(None, None) - self.assertRaises(NotImplementedError, ir.register, "uaid", {}) + self.assertRaises(NotImplementedError, ir.register, "uaid", {}, "") self.assertRaises(NotImplementedError, ir.route_notification, "uaid", {}) self.assertRaises(NotImplementedError, ir.amend_msg, {}) @@ -79,32 +80,55 @@ def init(self, settings, router_conf): class APNSRouterTestCase(unittest.TestCase): - def setUp(self): + + @patch("twisted.internet.reactor.callLater") + def setUp(self, cl): from twisted.logger import Logger settings = AutopushSettings( hostname="localhost", statsd_host=None, ) - apns_config = {'cert_file': 'fake.cert', 'key_file': 'fake.key'} + apns_config = {'firefox': {'cert': 'fake.cert', 'key': 'fake.key'}} self.mock_apns = Mock(spec=apns.APNs) self.router = APNSRouter(settings, apns_config) - self.router.apns = self.mock_apns + self.router.apns['firefox'] = self.mock_apns self.router.log = Mock(spec=Logger) self.headers = {"content-encoding": "aesgcm", "encryption": "test", "encryption-key": "test"} self.notif = Notification(10, "q60d6g", dummy_chid, self.headers, 200) - self.router_data = dict(router_data=dict(token="connect_data")) + self.router_data = dict(router_data=dict(token="connect_data", + rel_channel="firefox")) + + @patch("twisted.internet.reactor.callLater") + def test_with_max(self, cl): + settings = AutopushSettings( + hostname="localhost", + statsd_host=None, + ) + apns_config = {'max_messages': 2, + 'firefox': {'cert': 'fake.cert', 'key': 'fake.key'}} + self.mock_apns = Mock(spec=apns.APNs) + self.router = APNSRouter(settings, apns_config) + eq_(self.router._max_messages, 2) + ok_('max_messages' not in self.router._config) def test_register(self): result = self.router.register("uaid", - router_data={"token": "connect_data"}) - eq_(result, {"token": "connect_data"}), + router_data={"token": "connect_data"}, + app_id="firefox") + eq_(result, {"rel_channel": "firefox", "token": "connect_data"}) def test_register_bad(self): - self.assertRaises(RouterException, self.router.register, "uaid", - router_data={}) + self.assertRaises(RouterException, self.router.register, + "uaid", router_data={}, app_id="firefox") + + def test_register_bad_channel(self): + self.assertRaises(RouterException, self.router.register, + "uaid", + router_data={"token": "connect_data"}, + app_id="unknown") def test_route_notification(self): d = self.router.route_notification(self.notif, self.router_data) @@ -116,56 +140,97 @@ def check_results(result): d.addCallback(check_results) return d + def test_too_many_messages(self): + now = time.time() + self.router.messages = { + '123': { + 'time_sent': now, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}}, + } + + self.router._max_messages = 1 + d = self.router.route_notification(self.notif, self.router_data) + + def check_results(result): + ok_(isinstance(result, Failure)) + eq_(len(self.router.messages), 1) + eq_(result.value.status_code, 503) + + d.addBoth(check_results) + return d + def test_message_pruning(self): - now = int(time.time()) - self.router.messages = {now: {'token': 'dump', 'payload': {}}, - now-60: {'token': 'dump', 'payload': {}}} + now = time.time() + self.router.messages = {'123': { + 'time_sent': now-60, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}} + } d = self.router.route_notification(self.notif, self.router_data) def check_results(result): ok_(isinstance(result, RouterResponse)) + # presume that the timer clicked. + self.router._cleanup() assert(self.mock_apns.gateway_server.send_notification.called) eq_(len(self.router.messages), 1) - payload = self.router.messages[now]['payload'] - eq_(payload.alert, 'SimplePush') + messages = self.router.messages + + payload = messages[messages.keys()[-1]]['payload'] + eq_(payload.alert, 'Mozilla Push') custom = payload.custom - eq_(custom['Msg'], self.notif.data) - eq_(custom['Ver'], self.notif.version) - eq_(custom['Con'], 'aesgcm') - eq_(custom['Enc'], 'test') - eq_(custom['Enckey'], 'test') - eq_(custom['Chid'], self.notif.channel_id) - ok_('Cryptokey' not in custom) + eq_(custom['body'], self.notif.data) + eq_(custom['ver'], self.notif.version) + eq_(custom['con'], 'aesgcm') + eq_(custom['enc'], 'test') + eq_(custom['enckey'], 'test') + eq_(custom['chid'], self.notif.channel_id) + ok_('cryptokey' not in custom) d.addCallback(check_results) return d def test_response_listener_with_success(self): - self.router.messages = {1: {'token': 'dump', 'payload': {}}} + self.router.messages = {1: {'time_sent': 1, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}}} self.router._error(dict(status=0, identifier=1)) eq_(len(self.router.messages), 0) def test_response_listener_with_nonretryable_error(self): - self.router.messages = {1: {'token': 'dump', 'payload': {}}} + self.router.messages = {1: {'time_sent': 1, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}}} self.router._error(dict(status=2, identifier=1)) eq_(len(self.router.messages), 1) def test_response_listener_with_retryable_existing_message(self): - self.router.messages = {1: {'token': 'dump', 'payload': {}}} + self.router.messages = {'1': {'time_sent': 1, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}}} # Mock out the _connect call to be harmless - self.router._connect = Mock() - self.router._error(dict(status=1, identifier=1)) + self.router._error(dict(status=1, identifier='1')) eq_(len(self.router.messages), 1) - assert(self.router.apns.gateway_server.send_notification.called) + router = self.router.apns['firefox'] + assert(router.gateway_server.send_notification.called) def test_response_listener_with_retryable_non_existing_message(self): - self.router.messages = {1: {'token': 'dump', 'payload': {}}} + self.router.messages = {'1': {'time_sent': 1, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}}} self.router._error(dict(status=1, identifier=10)) eq_(len(self.router.messages), 1) - def test_ammend(self): + def test_amend(self): resp = {"key": "value"} eq_(resp, self.router.amend_msg(resp)) @@ -175,8 +240,10 @@ def test_route_crypto_key(self): "crypto-key": "test"} self.notif = Notification(10, "q60d6g", dummy_chid, headers, 200) now = int(time.time()) - self.router.messages = {now: {'token': 'dump', 'payload': {}}, - now-60: {'token': 'dump', 'payload': {}}} + self.router.messages = {'0': {'time_sent': now-60, + 'rel_channel': 'firefox', + 'token': 'dump', + 'payload': {}}} d = self.router.route_notification(self.notif, self.router_data) def check_results(result): @@ -185,19 +252,21 @@ def check_results(result): eq_(result.logged_status, 200) ok_("TTL" in result.headers) assert(self.mock_apns.gateway_server.send_notification.called) + self.router._cleanup() eq_(len(self.router.messages), 1) - payload = self.router.messages[now]['payload'] - eq_(payload.alert, 'SimplePush') + messages = self.router.messages + payload = messages[messages.keys()[-1]]['payload'] + eq_(payload.alert, 'Mozilla Push') custom = payload.custom - eq_(custom['Msg'], self.notif.data) - eq_(custom['Ver'], self.notif.version) - eq_(custom['Con'], 'aesgcm') - eq_(custom['Enc'], 'test') - eq_(custom['Cryptokey'], 'test') - eq_(custom['Chid'], self.notif.channel_id) - ok_('Enckey' not in custom) + eq_(custom['body'], self.notif.data) + eq_(custom['ver'], self.notif.version) + eq_(custom['con'], 'aesgcm') + eq_(custom['enc'], 'test') + eq_(custom['cryptokey'], 'test') + eq_(custom['chid'], self.notif.channel_id) + ok_('enckey' not in custom) d.addCallback(check_results) return d @@ -253,7 +322,7 @@ def test_init(self): def test_register(self): result = self.router.register("uaid", router_data={"token": "test123"}, - reg_id="test123") + app_id="test123") # Check the information that will be recorded for this user eq_(result, {"token": "test123", "creds": {"senderID": "test123", @@ -261,12 +330,16 @@ def test_register(self): def test_register_bad(self): self.assertRaises(RouterException, self.router.register, + "uaid", router_data={}, app_id="") + self.assertRaises(RouterException, + self.router.register, "uaid", - router_data={}) + router_data={}, + app_id='') self.assertRaises(RouterException, self.router.register, "uaid", router_data={"token": "abcd1234"}, - reg_id="invalid123") + app_id="invalid123") @patch("gcmclient.GCM") def test_gcmclient_fail(self, fgcm): @@ -452,7 +525,7 @@ def check_results(fail): def test_amend(self): self.router.register("uaid", router_data={"token": "test123"}, - reg_id="test123") + app_id="test123") resp = {"key": "value"} result = self.router.amend_msg(resp, self.router_data.get('router_data')) @@ -461,9 +534,8 @@ def test_amend(self): def test_register_invalid_token(self): self.assertRaises(RouterException, self.router.register, - "uaid", - router_data={"token": "invalid"}, - reg_id="invalid") + uaid="uaid", router_data={"token": "invalid"}, + app_id="invalid") class FCMRouterTestCase(unittest.TestCase): @@ -523,7 +595,7 @@ def throw_auth(*args, **kwargs): def test_register(self): result = self.router.register("uaid", router_data={"token": "test123"}, - reg_id="test123") + app_id="test123") # Check the information that will be recorded for this user eq_(result, {"token": "test123", "creds": {"senderID": "test123", @@ -700,8 +772,9 @@ def check_results(fail): return d def test_amend(self): - self.router.register("uaid", router_data={"token": "test123"}, - reg_id="test123") + self.router.register(uaid="uaid", + router_data={"token": "test123"}, + app_id="test123") resp = {"key": "value"} result = self.router.amend_msg(resp, self.router_data.get('router_data')) @@ -709,9 +782,9 @@ def test_amend(self): result) def test_register_invalid_token(self): - self.assertRaises(RouterException, self.router.register, "uaid", - router_data={"token": "invalid"}, - reg_id="invalid") + self.assertRaises(RouterException, self.router.register, + uaid="uaid", router_data={"token": "invalid"}, + app_id="invalid") class SimplePushRouterTestCase(unittest.TestCase): diff --git a/configs/autopush_endpoint.ini.sample b/configs/autopush_endpoint.ini.sample index d054e6e4..a09bf57b 100644 --- a/configs/autopush_endpoint.ini.sample +++ b/configs/autopush_endpoint.ini.sample @@ -30,10 +30,3 @@ port = 8082 ; autokey generator as the crypto_key argument, and sorted [newest, oldest] #auth_key = [HJVPy4ZwF4Yz_JdvXTL8hRcwIhv742vC60Tg5Ycrvw8=] -; Default hash of senderids and associated authorization keys. -; THIS WILL OVERWRITE ANY VALUES STORED IN S3! -; The list is specified as a JSON object formatted as: -; { :{"auth": }} -; e.g. -; {"12345": {"auth": "abcd_efg"}, "01357": {"auth": "ZYX=abc"}} -#senderid_list = diff --git a/configs/autopush_shared.ini.sample b/configs/autopush_shared.ini.sample index e67be0c3..74ac0462 100644 --- a/configs/autopush_shared.ini.sample +++ b/configs/autopush_shared.ini.sample @@ -115,3 +115,17 @@ endpoint_port = 8082 ; Uncomment to disable AWS meta checks. #no_aws +; Apple Push Notification System uses a set of certificates tied to the +; application. Since we support multiple iOS "applications" we need to support +; multiple certificates. +; apns_cert is a JSON formatted dict specifying the paths to the cert files, +; as well as any other bits of information that may be necessary. The format +; is {"platform": +; {"cert": "path_to_cert_file", +; "key": "path_to_key_file", +; "default_title": "(optional) default alert title 'Mozilla Push'", +; "sandbox": False}, // Use the APNs sandbox feature +; ... } +; e.g {"firefox": {"cert": "certs/main.cert", "key": "certs/main.key"}, "beta": {"cert": "certs/beta.cert", "key": "certs/beta.key"}} +#apns_creds = + diff --git a/docs/api.rst b/docs/api.rst index 1ebe3d06..8a4d3c24 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -20,7 +20,6 @@ documentation is organized alphabetically by module name. api/router/fcm api/router/interface api/router/simple - api/senderids api/settings api/ssl api/utils diff --git a/docs/api/router/simple.rst b/docs/api/router/simple.rst index 44cab744..2504795d 100644 --- a/docs/api/router/simple.rst +++ b/docs/api/router/simple.rst @@ -10,8 +10,3 @@ :special-members: __init__ :private-members: :member-order: bysource - -Utility Functions -+++++++++++++++++ - -.. autofunction:: node_key diff --git a/docs/api/senderids.rst b/docs/api/senderids.rst deleted file mode 100644 index d3b440e7..00000000 --- a/docs/api/senderids.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. _senderids_module: - -:mod:`autopush.senderids` -------------------------- - -.. automodule:: autopush.senderids - -Base Class -++++++++++ - -.. autoclass:: SenderIDs - :members: - :private-members: - :member-order: bysource - diff --git a/docs/http.rst b/docs/http.rst index 0d4b005f..64c84308 100644 --- a/docs/http.rst +++ b/docs/http.rst @@ -267,8 +267,9 @@ Each bridge may require a unique token that addresses the remote application For GCM/FCM, this is the `SenderID` (or 'project number') and is pre-negotiated outside of the push service. You can find this number using the `Google developer console `__. -For APNS, there is no client token, so this value is arbitrary. -Feel free to use "a" or "0" or any other valid URL path token. For our examples, we will use a client token of +For APNS, this value is the "platform" or "channel" of development (e.g. +"firefox", "beta", "gecko", etc.) +For our examples, we will use a client token of "33clienttoken33". :{instance_id}: The bridge specific private identifier token