def _process_reply(self, reply): """Process GCM send reply""" # acks: # for reg_id, msg_id in reply.success.items(): # updates for old_id, new_id in reply.canonical.items(): log.msg("GCM id changed : %s => " % old_id, new_id) return RouterResponse(status_code=503, response_body="Please try request again.", router_data=dict(token=new_id)) # naks: # uninstall: for reg_id in reply.not_registered: log.msg("GCM no longer registered: %s" % reg_id) return RouterResponse( status_code=410, response_body="Endpoint requires client update", router_data={}, ) # for reg_id, err_code in reply.failed.items(): if len(reply.failed.items()) > 0: log.msg("GCM failures: %s" % json.dumps(reply.failed.items())) raise RouterException("GCM failure to deliver", status_code=503, response_body="Please try request later.") # retries: if reply.needs_retry(): raise RouterException("GCM failure to deliver", status_code=503, response_body="Please try request later.") return RouterResponse(status_code=200, response_body="Message Sent")
def _process_reply(self, reply, notification, router_data, ttl): """Process FCM send reply""" # acks: # for reg_id, msg_id in reply.success.items(): # updates result = reply.get('results', [{}])[0] if reply.get('canonical_ids'): old_id = router_data['token'] new_id = result.get('registration_id') self.log.debug("FCM id changed : {old} => {new}", old=old_id, new=new_id) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="reregister")) return RouterResponse(status_code=503, response_body="Please try request again.", router_data=dict(token=new_id)) if reply.get('failure'): self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="failure")) reason = result.get('error', "Unreported") err = self.reasonTable.get(reason) if err.get("crit", False): self.log.critical( err['msg'], nlen=notification.data_length, regid=router_data["token"], senderid=self.senderID, ttl=notification.ttl, ) raise RouterException("FCM failure to deliver", status_code=err['err'], response_body="Please try request " "later.", log_exception=False) creds = router_data["creds"] self.log.debug("{msg} : {info}", msg=err['msg'], info={"senderid": creds.get('registration_id'), "reason": reason}) return RouterResponse( status_code=err['err'], errno=err['errno'], response_body=err['msg'], router_data={}, ) self.metrics.increment("notification.bridge.sent", self._base_tags) self.metrics.gauge("notification.message_data", notification.data_length, tags=make_tags(self._base_tags, destination="Direct")) location = "%s/m/%s" % (self.ap_settings.endpoint_url, notification.version) return RouterResponse(status_code=201, response_body="", headers={"TTL": ttl, "Location": location}, logged_status=200)
def _process_reply(self, reply, uaid_data, ttl, notification): """Process GCM send reply""" # acks: # for reg_id, msg_id in reply.success.items(): # updates for old_id, new_id in reply.canonical.items(): self.log.info("GCM id changed : {old} => {new}", old=old_id, new=new_id) self.metrics.increment("updates.client.bridge.gcm.failed.rereg", self._base_tags) return RouterResponse(status_code=503, response_body="Please try request again.", router_data=dict(token=new_id)) # naks: # uninstall: for reg_id in reply.not_registered: self.metrics.increment("updates.client.bridge.gcm.failed.unreg", self._base_tags) self.log.info("GCM no longer registered: %s" % reg_id) return RouterResponse( status_code=410, response_body="Endpoint requires client update", router_data={}, ) # for reg_id, err_code in reply.failed.items(): if len(reply.failed.items()) > 0: self.metrics.increment("updates.client.bridge.gcm.failed.failure", self._base_tags) self.log.info("GCM failures: {failed()}", failed=lambda: repr(reply.failed.items())) raise RouterException("GCM unable to deliver", status_code=410, response_body="GCM recipient not available.", log_exception=False, ) # retries: if reply.needs_retry(): self.metrics.increment("updates.client.bridge.gcm.failed.retry", self._base_tags) self.log.warn("GCM retry requested: {failed()}", failed=lambda: repr(reply.failed.items())) raise RouterException("GCM failure to deliver, retry", status_code=503, response_body="Please try request later.", log_exception=False) self.metrics.increment("updates.client.bridge.gcm.succeeded", self._base_tags) location = "%s/m/%s" % (self.ap_settings.endpoint_url, notification.version) return RouterResponse(status_code=201, response_body="", headers={"TTL": ttl, "Location": location}, logged_status=200)
def test_router_returns_data_without_detail(self): self.ap_settings.parse_endpoint = Mock(return_value=dict( uaid=dummy_uaid, chid=dummy_chid, public_key="asdfasdf", )) self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( uaid=dummy_uaid, router_type="webpush", router_data=dict(uaid="uaid"), current_month=self.ap_settings.current_msg_month, ) self.wp_router_mock.route_notification.return_value = RouterResponse( status_code=503, router_data=dict(), ) def handle_finish(result): eq_(result, True) self.wp.set_status.assert_called_with(503, reason=None) ok_(self.router_mock.drop_user.called) self.finish_deferred.addCallback(handle_finish) self.wp.post("v1", dummy_token) return self.finish_deferred
def stored_response(self, notification): location = "%s/m/%s" % (self.ap_settings.endpoint_url, notification.location) return RouterResponse(status_code=201, response_body="", headers={"Location": location, "TTL": notification.ttl}, logged_status=202)
def _route(self, notification, router_data): """Blocking APNS call to route the notification""" token = router_data["token"] custom = { "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"] if "crypto-key" in notification.headers: 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.apns.gateway_server.send_notification(token, payload, now) # 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] return RouterResponse(status_code=200, response_body="Message sent")
def test_router_needs_update(self): self.ap_settings.parse_endpoint = Mock(return_value=dict( uaid=dummy_uaid, chid=dummy_chid, public_key="asdfasdf", )) self.fernet_mock.decrypt.return_value = dummy_token self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), uaid=dummy_uaid, current_month=self.ap_settings.current_msg_month, ) self.wp_router_mock.route_notification.return_value = RouterResponse( status_code=503, router_data=dict(token="new_connect"), ) def handle_finish(result): eq_(result, True) self.wp.set_status.assert_called_with(503, reason=None) ru = self.router_mock.register_user ok_(ru.called) eq_('webpush', ru.call_args[0][0].get('router_type')) self.finish_deferred.addCallback(handle_finish) self.wp.post("v1", dummy_token) return self.finish_deferred
def test_router_needs_update(self): self.conf.parse_endpoint = Mock(return_value=dict( uaid=dummy_uaid, chid=dummy_chid, public_key="asdfasdf", )) self.fernet_mock.decrypt.return_value = dummy_token self.db.router.get_uaid = Mock(return_value=dict( router_type="webpush", router_data=dict(), uaid=dummy_uaid, current_month=self.db.current_msg_month, )) self.db.router.register_user = Mock(return_value=False) self.wp_router_mock.route_notification.return_value = RouterResponse( status_code=503, router_data=dict(token="new_connect"), ) self.db.message_tables.append(self.db.current_msg_month) resp = yield self.client.post( self.url(api_ver="v1", token=dummy_token), ) assert resp.get_status() == 503 ru = self.db.router.register_user assert ru.called assert 'webpush' == ru.call_args[0][0].get('router_type')
def stored_response(self, notification): self.metrics.increment("notification.message_data", notification.data_length, tags=make_tags(destination='Direct')) location = "%s/m/%s" % (self.conf.endpoint_url, notification.location) return RouterResponse(status_code=201, response_body="", headers={"Location": location, "TTL": notification.ttl}, logged_status=202)
def test_webpush_uaid_lookup_no_crypto_headers_without_data(self): fresult = dict(router_type="webpush") frouter = self.settings.routers["webpush"] frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): assert(frouter.route_notification.called) self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def test_put_default_router(self): self.fernet_mock.decrypt.return_value = "123:456" self.router_mock.get_uaid.return_value = dict() self.sp_router_mock.route_notification.return_value = RouterResponse() def handle_finish(result): self.assertTrue(result) self.endpoint.set_status.assert_called_with(200) self.finish_deferred.addCallback(handle_finish) self.endpoint.put(dummy_uaid) return self.finish_deferred
def test_webpush_uaid_lookup_no_crypto_headers_with_data(self): fresult = dict(router_type="webpush") frouter = self.settings.routers["webpush"] frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.request_mock.body = b"stuff" self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): self.endpoint.set_status.assert_called_with(400) self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def delivered_response(self, notification): self.metrics.gauge("notification.message_data", notification.data_length, tags=make_tags(destination='Stored')) location = "%s/m/%s" % (self.ap_settings.endpoint_url, notification.location) return RouterResponse(status_code=201, response_body="", headers={ "Location": location, "TTL": notification.ttl or 0 }, logged_status=200)
def stored_response(self, notification): self.metrics.increment("notification.message_data", notification.data_length, tags=make_tags(destination='Stored')) location = "%s/m/%s" % (self.conf.endpoint_url, notification.location) # RFC https://tools.ietf.org/html/rfc8030#section-5 # all responses should be 201, unless this is a push reciept request, # which requires a 202 and a URL that can be checked later for UA # acknowledgement. (We don't support that yet. See autopush-rs#244) return RouterResponse(status_code=201, response_body="", headers={"Location": location, "TTL": notification.ttl}, logged_status=201)
def test_other_uaid_lookup_no_crypto_headers(self): fresult = dict(router_type="test") frouter = Mock(spec=Router) frouter.route_notification = Mock() frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.endpoint.ap_settings.routers["test"] = frouter self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): assert(frouter.route_notification.called) self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def test_uaid_lookup_results(self): fresult = dict(router_type="test") frouter = Mock(spec=Router) frouter.route_notification = Mock() frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" self.endpoint.ap_settings.routers["test"] = frouter self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): assert(frouter.route_notification.called) self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def _process_reply(self, reply, notification, router_data, ttl): """Process FCM send reply""" # Failures are returned as non-200 messages (404, 410, etc.) self.metrics.increment("notification.bridge.sent", tags=self._base_tags) self.metrics.increment("notification.message_data", notification.data_length, tags=make_tags(self._base_tags, destination="Direct")) location = "%s/m/%s" % (self.conf.endpoint_url, notification.version) return RouterResponse(status_code=201, response_body="", headers={ "TTL": ttl, "Location": location }, logged_status=200)
def test_put_router_needs_update(self): self.fernet_mock.decrypt.return_value = "123:456" self.router_mock.get_uaid.return_value = dict( router_type="simplepush", router_data=dict(), ) self.sp_router_mock.route_notification.return_value = RouterResponse( status_code=503, router_data=dict(token="new_connect"), ) def handle_finish(result): self.assertTrue(result) self.endpoint.set_status.assert_called_with(503) assert(self.router_mock.register_user.called) self.finish_deferred.addCallback(handle_finish) self.endpoint.put(dummy_uaid) return self.finish_deferred
def test_webpush_payload_encoding(self): fresult = dict(router_type="webpush") frouter = self.settings.routers["webpush"] frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" self.request_mock.body = b"\xc3\x28\xa0\xa1" self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): calls = frouter.route_notification.mock_calls eq_(len(calls), 1) (_, (notification, _), _) = calls[0] eq_(notification.channel_id, "fred") eq_(notification.data, b"wyigoQ==") self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def test_other_payload_encoding(self): fresult = dict(router_type="test") frouter = Mock(spec=Router) frouter.route_notification = Mock() frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.endpoint.ap_settings.routers["test"] = frouter self.request_mock.body = b"stuff" self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): calls = frouter.route_notification.mock_calls eq_(len(calls), 1) (_, (notification, _), _) = calls[0] eq_(notification.channel_id, "fred") eq_(notification.data, b"stuff") self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def test_webpush_ttl_too_large(self): from autopush.endpoint import MAX_TTL fresult = dict(router_type="test") frouter = Mock(spec=Router) frouter.route_notification = Mock() frouter.route_notification.return_value = RouterResponse() self.endpoint.chid = "fred" self.request_mock.headers["ttl"] = MAX_TTL + 100 self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" self.endpoint.ap_settings.routers["test"] = frouter self.endpoint._uaid_lookup_results(fresult) def handle_finish(value): assert(frouter.route_notification.called) args, kwargs = frouter.route_notification.call_args notif = args[0] eq_(notif.ttl, MAX_TTL) self.finish_deferred.addCallback(handle_finish) return self.finish_deferred
def test_router_returns_data_without_detail(self): self.conf.parse_endpoint = Mock(return_value=dict( uaid=dummy_uaid, chid=dummy_chid, public_key="asdfasdf", )) self.fernet_mock.decrypt.return_value = dummy_token self.db.router.get_uaid.return_value = dict( uaid=dummy_uaid, router_type="webpush", router_data=dict(uaid="uaid"), current_month=self.db.current_msg_month, ) self.wp_router_mock.route_notification.return_value = RouterResponse( status_code=503, router_data=dict(), ) resp = yield self.client.post( self.url(api_ver="v1", token=dummy_token), ) assert resp.get_status() == 503 assert self.db.router.drop_user.called
def test_post_webpush_with_headers_in_response(self): self.fernet_mock.decrypt.return_value = "123:456" self.endpoint.set_header = Mock() self.request_mock.headers["encryption"] = "stuff" self.request_mock.headers["content-encoding"] = "aes128" self.router_mock.get_uaid.return_value = dict( router_type="webpush", router_data=dict(), ) self.wp_router_mock.route_notification.return_value = RouterResponse( status_code=201, headers={"Location": "Somewhere"} ) def handle_finish(result): self.assertTrue(result) self.endpoint.set_status.assert_called_with(201) self.endpoint.set_header.assert_called_with( "Location", "Somewhere") self.finish_deferred.addCallback(handle_finish) self.endpoint.post(dummy_uaid) return self.finish_deferred
def test_router_response_client_error(self): from autopush.router.interface import RouterResponse response = RouterResponse(headers=dict(Location="http://a.com/"), status_code=400) self.base._router_response(response, None, None) self.status_mock.assert_called_with(400, reason=None)
def test_router_response(self): from autopush.router.interface import RouterResponse response = RouterResponse(headers=dict(Location="http://a.com/")) self.base._router_response(response) self.status_mock.assert_called_with(200, reason=None)
def _route(self, notification, uaid_data): """Blocking ADM call to route the notification""" router_data = uaid_data["router_data"] # THIS MUST MATCH THE CHANNELID GENERATED BY THE REGISTRATION SERVICE # Currently this value is in hex form. data = {"chid": notification.channel_id.hex} # Payload data is optional. The endpoint handler validates that the # correct encryption headers are included with the data. if notification.data: data['body'] = notification.data data['con'] = notification.headers['encoding'] if 'encryption' in notification.headers: data['enc'] = notification.headers.get('encryption') if 'crypto_key' in notification.headers: data['cryptokey'] = notification.headers['crypto_key'] # registration_ids are the ADM instance tokens (specified during # registration. ttl = min(self.MAX_TTL, max(notification.ttl or 0, self.min_ttl)) try: adm = self.profiles[router_data['creds']['profile']] adm.send(reg_id=router_data.get("token"), payload=data, collapseKey=notification.topic, ttl=ttl) except RouterException: raise # pragma nocover except Timeout as e: self.log.warn("ADM Timeout: %s" % e) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="timeout")) raise RouterException("Server error", status_code=502, errno=902, log_exception=False) except ConnectionError as e: self.log.warn("ADM Unavailable: %s" % e) self.metrics.increment("notification.bridge.error", tags=make_tags( self._base_tags, reason="connection_unavailable")) raise RouterException("Server error", status_code=502, errno=902, log_exception=False) except ADMAuthError as e: self.log.error("ADM unable to authorize: %s" % e) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="auth failure")) raise RouterException("Server error", status_code=500, errno=902, log_exception=False) except Exception as e: self.log.error("Unhandled exception in ADM Routing: %s" % e) raise RouterException("Server error", status_code=500) location = "%s/m/%s" % (self.conf.endpoint_url, notification.version) return RouterResponse(status_code=201, response_body="", headers={ "TTL": ttl, "Location": location }, logged_status=200)
def stored_response(self, notification): return RouterResponse(202, "Notification Stored")
def delivered_response(self, notification): return RouterResponse(200, "Delivered")
def _process_reply(self, reply, uaid_data, ttl, notification): """Process GCM send reply""" # acks: # for reg_id, msg_id in reply.success.items(): # updates for old_id, new_id in reply.canonicals.items(): self.log.debug("GCM id changed : {old} => {new}", old=old_id, new=new_id) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="reregister")) return RouterResponse(status_code=503, response_body="Please try request again.", router_data=dict(token=new_id)) # naks: # uninstall: for reg_id in reply.not_registered: self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="unregistered")) self.log.debug("GCM no longer registered: %s" % reg_id) return RouterResponse( status_code=410, response_body="Endpoint requires client update", router_data={}, ) # for reg_id, err_code in reply.failed.items(): if len(reply.failed.items()) > 0: self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="failure")) self.log.debug("GCM failures: {failed()}", failed=lambda: repr(reply.failed.items())) raise RouterException("GCM unable to deliver", status_code=410, response_body="GCM recipient not available.", log_exception=False, ) # retries: if reply.retry_after: self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="retry")) self.log.warn("GCM retry requested: {failed()}", failed=lambda: repr(reply.failed.items())) raise RouterException("GCM failure to deliver, retry", status_code=503, headers={"Retry-After": reply.retry_after}, response_body="Please try request " "in {} seconds.".format( reply.retry_after ), log_exception=False) self.metrics.increment("notification.bridge.sent", tags=self._base_tags) self.metrics.increment("notification.message_data", notification.data_length, tags=make_tags(self._base_tags, destination='Direct')) location = "%s/m/%s" % (self.conf.endpoint_url, notification.version) return RouterResponse(status_code=201, response_body="", headers={"TTL": ttl, "Location": location}, logged_status=200)
def _route(self, notification, router_data): """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"] apns_client = self.apns[rel_channel] # chid MUST MATCH THE CHANNELID GENERATED BY THE REGISTRATION SERVICE # Currently this value is in hex form. payload = { "chid": notification.channel_id.hex, "ver": notification.version, } if notification.data: payload["body"] = notification.data payload["con"] = notification.headers["encoding"] payload["enc"] = notification.headers["encryption"] if "crypto_key" in notification.headers: payload["cryptokey"] = notification.headers["crypto_key"] elif "encryption_key" in notification.headers: payload["enckey"] = notification.headers["encryption_key"] payload['aps'] = router_data.get( 'aps', { "mutable-content": 1, "alert": { "loc-key": "SentTab.NoTabArrivingNotification.body", "title-loc-key": "SentTab.NoTabArrivingNotification.title", } }) apns_id = str(uuid.uuid4()).lower() # APNs may force close a connection on us without warning. # if that happens, retry the message. success = False try: apns_client.send(router_token=router_token, payload=payload, apns_id=apns_id) success = True except ConnectionError: self.metrics.increment("notification.bridge.connection.error", tags=make_tags(self._base_tags, application=rel_channel, reason="connection_error")) except (HTTP20Error, socket.error): self.metrics.increment("notification.bridge.connection.error", tags=make_tags(self._base_tags, application=rel_channel, reason="http2_error")) if not success: raise RouterException( "Server error", status_code=502, response_body="APNS returned an error processing request", log_exception=False, ) location = "%s/m/%s" % (self.conf.endpoint_url, notification.version) self.metrics.increment("notification.bridge.sent", tags=make_tags(self._base_tags, application=rel_channel)) self.metrics.increment("updates.client.bridge.apns.{}.sent".format( router_data["rel_channel"]), tags=self._base_tags) self.metrics.increment("notification.message_data", notification.data_length, tags=make_tags(self._base_tags, destination='Direct')) return RouterResponse(status_code=201, response_body="", headers={ "TTL": notification.ttl, "Location": location }, logged_status=200)