def _process_error(self, failure): err = failure.value if isinstance(err, gcmclient.GCMAuthenticationError): self.log.error("GCM Authentication Error: %s" % err) raise RouterException("Server error", status_code=500, errno=901) if isinstance(err, TimeoutError): self.log.warn("GCM Timeout: %s" % err) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="timeout")) raise RouterException("Server error", status_code=502, errno=903, log_exception=False) if isinstance(err, ConnectError): self.log.warn("GCM Unavailable: %s" % err) 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) return failure
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_error(self, failure): err = failure.value if isinstance(err, FCMAuthenticationError): self.log.error("FCM Authentication Error: {}".format(err)) raise RouterException("Server error", status_code=500, errno=901) if isinstance(err, TimeoutError): self.log.warn("FCM Timeout: %s" % err) self.metrics.increment("notification.bridge.error", tags=make_tags( self._base_tags, reason="timeout", error=502, errno=903, )) raise RouterException("Server error", status_code=502, errno=903, log_exception=False) if isinstance(err, ConnectError): self.log.warn("FCM Unavailable: %s" % err) self.metrics.increment("notification.bridge.error", tags=make_tags( self._base_tags, reason="connection_unavailable", error=502, errno=902, )) raise RouterException("Server error", status_code=502, errno=902, log_exception=False) if isinstance(err, FCMNotFoundError): self.log.debug("FCM Recipient not found: %s" % err) self.metrics.increment("notification.bridge.error", tags=make_tags( self._base_tags, reason="recipient_gone", error=404, errno=106, )) raise RouterException("FCM Recipient no longer available", status_code=404, errno=106, log_exception=False) if isinstance(err, RouterException): self.log.warn("FCM Error: {}".format(err)) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, reason="server_error", error=502, errno=0)) return failure
def _route(self, notification, router_data): """Blocking FCM call to route the notification""" # THIS MUST MATCH THE CHANNELID GENERATED BY THE REGISTRATION SERVICE # Currently this value is in hex form. data = {"chid": notification.channel_id.hex} if not router_data.get("token"): raise self._error("No registration token found. " "Rejecting message.", 410, errno=106, log_exception=False) regid = router_data.get("token") # Payload data is optional. The endpoint handler validates that the # correct encryption headers are included with the data. if notification.data: mdata = self.config.get('max_data', 4096) if notification.data_length > mdata: raise self._error("This message is intended for a " + "constrained device and is limited " + "to 3070 bytes. Converted buffer too " + "long by %d bytes" % (notification.data_length - mdata), 413, errno=104, log_exception=False) data['body'] = notification.data data['con'] = notification.headers['encoding'] if 'encryption' in notification.headers: data['enc'] = notification.headers['encryption'] if 'crypto_key' in notification.headers: data['cryptokey'] = notification.headers['crypto_key'] elif 'encryption_key' in notification.headers: data['enckey'] = notification.headers['encryption_key'] # registration_ids are the FCM instance tokens (specified during # registration. router_ttl = min(self.MAX_TTL, max(self.min_ttl, notification.ttl or 0)) try: result = self.fcm.notify_single_device( collapse_key=self.collapseKey, data_message=data, dry_run=self.dryRun or ('dryrun' in router_data), registration_id=regid, time_to_live=router_ttl, ) except pyfcm.errors.AuthenticationError as e: self.log.error("Authentication Error: %s" % e) raise RouterException("Server error", status_code=500) except ConnectionError as e: self.metrics.increment("notification.bridge.error", tags=make_tags( self._base_tags, reason="connection_unavailable")) self.log.warn("Could not connect to FCM server: %s" % e) raise RouterException("Server error", status_code=502, log_exception=False) except Exception as e: self.log.error("Unhandled FCM Error: %s" % e) raise RouterException("Server error", status_code=500) return self._process_reply(result, notification, router_data, ttl=router_ttl)
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 _router_completed(self, response, uaid_data, warning="", router_type=None, vapid=None): """Called after router has completed successfully""" # Log the time taken for routing self._timings["route_time"] = time.time() - self._router_time # Were we told to update the router data? time_diff = time.time() - self._start_time if response.router_data is not None: if not response.router_data: # An empty router_data object indicates that the record should # be deleted. There is no longer valid route information for # this record. self.log.debug(format="Dropping User", code=100, uaid_hash=hasher(uaid_data["uaid"]), uaid_record=dump_uaid(uaid_data), client_info=self._client_info) d = deferToThread(self.db.router.drop_user, uaid_data["uaid"]) d.addCallback(lambda x: self._router_response(response)) return d # The router data needs to be updated to include any changes # requested by the bridge system uaid_data["router_data"] = response.router_data # set the AWS mandatory data uaid_data["connected_at"] = ms_time() d = deferToThread(self.db.router.register_user, uaid_data) response.router_data = None d.addCallback(lambda x: self._router_completed( response, uaid_data, warning, router_type, vapid)) return d else: # No changes are requested by the bridge system, proceed as normal dest = 'Direct' if response.status_code == 200 or response.logged_status == 200: self.log.debug(format="Successful delivery", client_info=self._client_info) elif response.status_code == 202 or response.logged_status == 202: self.log.debug(format="Router miss, message stored.", client_info=self._client_info) dest = 'Stored' self.metrics.timing("notification.request_time", duration=time_diff) self.metrics.increment('notification.message.success', tags=make_tags(destination=dest, router=router_type, vapid=(vapid is not None))) response.response_body = (response.response_body + " " + warning).strip() self._router_response(response)
def validate_uaid_month_and_chid(self, d): db = self.context["db"] # type: DatabaseManager try: result = db.router.get_uaid(d["uaid"].hex) except ItemNotFound: raise InvalidRequest("UAID not found", status_code=410, errno=103) # We must have a router_type to validate the user router_type = result.get("router_type") if router_type not in VALID_ROUTER_TYPES: self.context["log"].debug(format="Dropping User", code=102, uaid_hash=hasher(result["uaid"]), uaid_record=repr(result)) self.context["metrics"].increment("updates.drop_user", tags=make_tags(errno=102)) self.context["db"].router.drop_user(result["uaid"]) raise InvalidRequest("No such subscription", status_code=410, errno=106) if (router_type == "gcm" and 'senderID' not in result.get( 'router_data', {}).get("creds", {})): # Make sure we note that this record is bad. result['critical_failure'] = \ result.get('critical_failure', "Missing SenderID") db.router.register_user(result) if (router_type == "fcm" and 'app_id' not in result.get('router_data', {})): # Make sure we note that this record is bad. result['critical_failure'] = \ result.get('critical_failure', "Missing SenderID") db.router.register_user(result) if result.get("critical_failure"): raise InvalidRequest("Critical Failure: %s" % result.get("critical_failure"), status_code=410, errno=105) # Some stored user records are marked as "simplepush". # If you encounter one, may need to tweak it a bit to get it as # a valid WebPush record. if result["router_type"] == "simplepush": result["router_type"] = "webpush" if result["router_type"] == "webpush": self._validate_webpush(d, result) # Propagate the looked up user data back out d["user_data"] = result
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 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 _validate_webpush(self, d, result): db = self.context["db"] # type: DatabaseManager log = self.context["log"] # type: Logger metrics = self.context["metrics"] # type: Metrics channel_id = normalize_id(d["chid"]) uaid = result["uaid"] if 'current_month' not in result: log.debug(format="Dropping User", code=102, uaid_hash=hasher(uaid), uaid_record=repr(result)) metrics.increment("updates.drop_user", tags=make_tags(errno=102)) db.router.drop_user(uaid) raise InvalidRequest("No such subscription", status_code=410, errno=106) month_table = result["current_month"] if month_table not in db.message_tables: log.debug(format="Dropping User", code=103, uaid_hash=hasher(uaid), uaid_record=repr(result)) metrics.increment("updates.drop_user", tags=make_tags(errno=103)) db.router.drop_user(uaid) raise InvalidRequest("No such subscription", status_code=410, errno=106) msg = db.message_table(month_table) exists, chans = msg.all_channels(uaid=uaid) if (not exists or channel_id.lower() not in map( lambda x: normalize_id(x), chans)): log.debug("Unknown subscription: {channel_id}", channel_id=channel_id) raise InvalidRequest("No such subscription", status_code=410, errno=106)
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 _router_completed(self, response, uaid_data, warning=""): """Called after router has completed successfully""" dest = 'Direct' if response.status_code == 200 or response.logged_status == 200: self.log.debug(format="Successful delivery", client_info=self._client_info) elif response.status_code == 202 or response.logged_status == 202: self.log.debug(format="Router miss, message stored.", client_info=self._client_info) dest = 'Stored' time_diff = time.time() - self._start_time self.metrics.timing("notification.request_time", duration=time_diff) self.metrics.increment('notification.message.success', tags=make_tags(destination=dest, router='simplepush')) response.response_body = (response.response_body + " " + warning).strip() self._router_response(response)
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, uaid_data): """Blocking GCM 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: mdata = self.router_conf.get('max_data', 4096) if notification.data_length > mdata: raise self._error("This message is intended for a " + "constrained device and is limited " + "to 3070 bytes. Converted buffer too " + "long by %d bytes" % (notification.data_length - mdata), 413, errno=104, log_exception=False) 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'] elif 'encryption_key' in notification.headers: data['enckey'] = notification.headers['encryption_key'] # registration_ids are the GCM instance tokens (specified during # registration. router_ttl = min(self.MAX_TTL, max(notification.ttl or 0, self.min_ttl)) payload = gcmclient.JSONMessage( registration_ids=[router_data.get("token")], collapse_key=self.collapseKey, time_to_live=router_ttl, dry_run=self.dryRun or ("dryrun" in router_data), data=data, ) try: gcm = self.gcm[router_data['creds']['senderID']] result = gcm.send(payload) except RouterException: raise # pragma nocover except KeyError: self.log.critical("Missing GCM bridge credentials") raise RouterException("Server error", status_code=500, errno=900) except gcmclient.GCMAuthenticationError as e: self.log.error("GCM Authentication Error: %s" % e) raise RouterException("Server error", status_code=500, errno=901) except ConnectionError as e: self.log.warn("GCM 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 Exception as e: self.log.error("Unhandled exception in GCM Routing: %s" % e) raise RouterException("Server error", status_code=500) return self._process_reply(result, uaid_data, ttl=router_ttl, notification=notification)
def delivered_response(self, notification): self.metrics.gauge("notification.message_data", notification.data_length, tags=make_tags(destination='Direct')) return RouterResponse(200, "Delivered")
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["con"] = notification.headers.get( "content-encoding", notification.headers.get("encoding")) if payload["con"] != "aes128gcm": if "encryption" in notification.headers: 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["body"] = notification.data 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. try: apns_client.send(router_token=router_token, payload=payload, apns_id=apns_id) except Exception as e: # We sometimes see strange errors around sending push notifications # to APNS. We get reports that after a new deployment things work, # but then after a week or so, messages across the APNS bridge # start to fail. The connections appear to be working correctly, # so we don't think that this is a problem related to how we're # connecting. if isinstance(e, ConnectionError): reason = "connection_error" elif isinstance(e, (HTTP20Error, socket.error)): reason = "http2_error" else: reason = "unknown" if isinstance(e, RouterException) and e.status_code in [404, 410]: raise RouterException( str(e), status_code=e.status_code, errno=106, response_body="User is no longer registered", log_exception=False) self.metrics.increment("notification.bridge.error", tags=make_tags(self._base_tags, application=rel_channel, reason=reason)) raise RouterException( str(e), status_code=502, response_body="APNS returned an error processing request", ) 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)
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)
def stored_response(self, notification): self.metrics.gauge("notification.message_data", notification.data_length, tags=make_tags(destination='Stored')) return RouterResponse(202, "Notification Stored")
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)