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 _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 _save_notification(self, uaid_data, notification): """Saves a notification, returns a deferred. This version of the overridden method saves each individual message to the message table along with relevant request headers if available. :type uaid_data: dict """ month_table = uaid_data["current_month"] if notification.ttl is None: # Note that this URL is temporary, as well as this warning as # we will 400 all missing TTL's eventually raise RouterException( "Missing TTL Header", response_body="Missing TTL Header, see: %s" % TTL_URL, status_code=400, errno=111, log_exception=False, ) if notification.ttl == 0: location = "%s/m/%s" % (self.ap_settings.endpoint_url, notification.version) raise RouterException("Finished Routing", status_code=201, log_exception=False, headers={"TTL": str(notification.ttl), "Location": location}, logged_status=204) return deferToThread( self.db.message_tables[month_table].store_message, notification=notification, )
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 _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 parse_response(self, content): # 400 will return an error message indicating what's wrong with the # javascript message you sent. # 403 is an error indicating that the client app is missing the # FCM Cloud Messaging permission (and a URL to set it) # Successful content body # { "name": "projects/.../messages/0:..."} # Failures: # { "error": # { "status": str # "message": str # "code": u64 # "details: [ # {"errorCode": str, # "@type": str}, # {"fieldViolations": [ # {"field": str, # "description": str} # ], # "type", str # } # ] # } # } # (Failures are a tad more verbose) if 500 <= self.code <= 599: self.retry_message = content return self try: data = json.loads(content) if self.code in (400, 403, 404, 410) or data.get('error'): # Having a hard time finding information about how some # things are handled in FCM, e.g. retransmit requests. # For now, catalog them as errors and provide back-pressure. err = data.get("error") if err.get("status") == "NOT_FOUND": raise FCMNotFoundError("FCM recipient no longer available") raise RouterException( "{}: {} {}".format( err.get("status"), err.get("message"), err.get("details", "No Details"))) if "name" in data: self.success = 1 except (TypeError, ValueError, KeyError, AttributeError): raise RouterException( "Unknown error response: {}".format(content)) return self
def _error(self, err, status, **kwargs): """Error handler that raises the RouterException""" self.log.debug(err, **kwargs) return RouterException(err, status_code=status, response_body=err, **kwargs)
def __init__(self, registration_ids, collapse_key, time_to_live, dry_run, data): """Convert data elements into a GCM payload. :param registration_ids: Single or list of registration ids to send to :type registration_ids: str or list :param collapse_key: GCM collapse key for the data. :type collapse_key: str :param time_to_live: Seconds to keep message alive :type time_to_live: int :param dry_run: GCM Dry run flag to allow remote verification :type dry_run: bool :param data: Data elements to send :type data: dict """ if not registration_ids: raise RouterException("No Registration IDs specified") if not isinstance(registration_ids, list): registration_ids = [registration_ids] self.registration_ids = registration_ids self.payload = { 'registration_ids': self.registration_ids, 'time_to_live': int(time_to_live), 'delay_while_idle': False, 'dry_run': bool(dry_run), } if collapse_key: self.payload["collapse_key"] = collapse_key if data: self.payload['data'] = data
def send(self, payload): """Send a payload to GCM :param payload: Dictionary of GCM formatted data :type payload: JSONMessage :return: Result """ headers = { 'Content-Type': 'application/json', 'Authorization': 'key={}'.format(self._api_key), } response = self._sender(url=self._endpoint, headers=headers, data=json.dumps(payload.payload), **self._options) if response.status_code in (400, 404): raise RouterException(response.content) if response.status_code == 401: raise GCMAuthenticationError("Authentication Error") if response.status_code == 200 or (500 <= response.status_code <= 599): return Result(payload, response)
def send(self, reg_id, payload, ttl=None, collapseKey=None): self.refresh_key() headers = { "Authorization": "Bearer {}".format(self._auth_token), "Content-Type": "application/json", "X-Amzn-Type-Version": "[email protected]", "X-Amzn-Accept-Type": "[email protected]", "Accept": "application/json", } data = {} if ttl: data["expiresAfter"] = ttl if collapseKey: data["consolidationKey"] = collapseKey data["data"] = payload url = ("https://api.amazon.com/messaging/registrations" "/{}/messages".format(reg_id)) resp = self._request.post( url, json=data, headers=headers, timeout=self._timeout, ) # in fine tradition, the response message can sometimes be less than # helpful. Still, good idea to include it anyway. if resp.status_code != 200: self._logger.error("Could not send ADM message: " + resp.text) raise RouterException(resp.content)
def error(self, failure): if isinstance(failure.value, (FCMAuthenticationError, FCMNotFoundError, TimeoutError, ConnectError)): raise failure.value self.logger.error("FCMv1Client failure: {}".format(failure.value)) raise RouterException("Server error: {}".format(failure.value))
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 test_router_fail_err(self): from autopush.exceptions import RouterException try: raise RouterException("error") except RouterException: fail = Failure() self.base._router_fail_err(fail) self.status_mock.assert_called_with(500, reason=None)
def test_router_fail_err_400_status(self): from autopush.exceptions import RouterException try: raise RouterException("Abort Ok", status_code=400) except RouterException: fail = Failure() self.base._router_fail_err(fail) self.status_mock.assert_called_with(400, reason=None)
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: client = self.gcmclients[router_data['creds']['senderID']] d = client.send(payload) d.addCallback( self._process_reply, uaid_data, router_ttl, notification) d.addErrback( self._process_error ) return d except KeyError: self.log.critical("Missing GCM bridge credentials") raise RouterException("Server error", status_code=500, errno=900)
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 test_put_bad_router_register(self): frouter = self.routers["test"] rexc = RouterException("invalid", status_code=402, errno=107) frouter.register = Mock(side_effect=rexc) resp = yield self.client.put(self.url(router_type='test', uaid=dummy_uaid.hex), headers={"Authorization": self.auth}, body=json.dumps(dict(token="blah"))) self._check_error(resp, rexc.status_code, rexc.errno, "")
def register(self, uaid, router_data, app_id, *args, **kwargs): # type: (str, JSONDict, str, *Any, **Any) -> None """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 :param router_data: Dict containing router specific configuration info :param app_id: The release channel identifier for cert info lookup """ 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=400, response_body="No token registered") router_data["rel_channel"] = app_id
def test_put_bad_router_register(self): frouter = self.reg.ap_settings.routers["test"] rexc = RouterException("invalid", status_code=402, errno=107) frouter.register = Mock(side_effect=rexc) def handle_finish(value): self._check_error(rexc.status_code, rexc.errno, "") self.finish_deferred.addCallback(handle_finish) self.reg.request.headers["Authorization"] = self.auth self.reg.put(self._make_req(router_type='test', uaid=dummy_uaid.hex)) return self.finish_deferred
def parse_response(self, content, code, message): # 401 handled in GCM.process() if code in (400, 404): raise RouterException(content) data = json.loads(content) if not data.get('results'): raise RouterException("Recv'd invalid response from GCM") reg_id = message.payload['registration_ids'][0] for res in data['results']: if 'message_id' in res: self.success[reg_id] = res['message_id'] if 'registration_id' in res: self.canonicals[reg_id] = res['registration_id'] else: if res['error'] in ['Unavailable', 'InternalServerError']: self.unavailable.append(reg_id) elif res['error'] == 'NotRegistered': self.not_registered.append(reg_id) else: self.failed[reg_id] = res['error'] return self
def test_post_bad_router_register(self, *args): router = self.routers["simplepush"] rexc = RouterException("invalid", status_code=402, errno=107) router.register = Mock(side_effect=rexc) resp = yield self.client.post(self.url(router_type="simplepush"), headers={"Authorization": self.auth}, body=json.dumps( dict( type="simplepush", channelID=str(dummy_chid), data={}, ))) self._check_error(resp, rexc.status_code, rexc.errno, "")
def test_post_bad_router_register(self, *args): frouter = Mock(spec=IRouter) self.reg.ap_settings.routers["simplepush"] = frouter rexc = RouterException("invalid", status_code=402, errno=107) frouter.register = Mock(side_effect=rexc) self.reg.request.body = json.dumps(dict( type="simplepush", channelID=str(dummy_chid), data={}, )) self.reg.request.uri = "/v1/xxx/yyy/register" self.reg.request.headers["Authorization"] = self.auth def handle_finish(value): self._check_error(rexc.status_code, rexc.errno, "") self.finish_deferred.addBoth(handle_finish) self.reg.post(self._make_req("simplepush", "", body=self.reg.request.body)) return self.finish_deferred
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 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=False) 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 _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 _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_notification(self, notification, uaid_data): """Route a notification to an internal node, and store it if the node can't deliver immediately or is no longer a valid node """ # Determine if they're connected at the moment node_id = uaid_data.get("node_id") uaid = uaid_data["uaid"] self.udp = uaid_data.get("udp") router = self.db.router # Node_id is present, attempt delivery. # - Send Notification to node # - Success: Done, return 200 # - Error (Node busy): Jump to Save notification below # - Error (Client gone, node gone/dead): Clear node entry for user # - Both: Done, return 503 if node_id: result = None try: result = yield self._send_notification(uaid, node_id, notification) except (ConnectError, ConnectionClosed, ResponseFailed, CancelledError) as exc: self.metrics.increment("updates.client.host_gone") yield deferToThread(router.clear_node, uaid_data).addErrback(self._eat_db_err) if isinstance(exc, ConnectionRefusedError): # Occurs if an IP record is now used by some other node # in AWS or if the connection timesout. self.log.debug("Could not route message: {exc}", exc=exc) if result and result.code == 200: self.metrics.increment("router.broadcast.hit") returnValue(self.delivered_response(notification)) # Save notification, node is not present or busy # - Save notification # - Success (older version): Done, return 202 # - Error (db error): Done, return 503 try: result = yield self._save_notification(uaid_data, notification) if result is False: self.metrics.increment("router.broadcast.miss") returnValue(self.stored_response(notification)) except JSONResponseError: raise RouterException("Error saving to database", status_code=503, response_body="Retry Request", errno=201) # - Lookup client # - Success (node found): Notify node of new notification # - Success: Done, return 200 # - Error (no client): Done, return 202 # - Error (no node): Clear node entry # - Both: Done, return 202 # - Success (no node): Done, return 202 # - Error (db error): Done, return 202 # - Error (no client) : Done, return 404 try: uaid_data = yield deferToThread(router.get_uaid, uaid) except JSONResponseError: self.metrics.increment("router.broadcast.miss") returnValue(self.stored_response(notification)) except ItemNotFound: self.metrics.increment("updates.client.deleted") raise RouterException("User was deleted", status_code=410, response_body="Invalid UAID", log_exception=False, errno=105) # Verify there's a node_id in here, if not we're done node_id = uaid_data.get("node_id") if not node_id: self.metrics.increment("router.broadcast.miss") returnValue(self.stored_response(notification)) try: result = yield self._send_notification_check(uaid, node_id) except (ConnectError, ConnectionClosed, ResponseFailed) as exc: self.metrics.increment("updates.client.host_gone") if isinstance(exc, ConnectionRefusedError): self.log.debug("Could not route message: {exc}", exc=exc) yield deferToThread( router.clear_node, uaid_data).addErrback(self._eat_db_err) self.metrics.increment("router.broadcast.miss") returnValue(self.stored_response(notification)) if result.code == 200: self.metrics.increment("router.broadcast.save_hit") returnValue(self.delivered_response(notification)) else: self.metrics.increment("router.broadcast.miss") ret_val = self.stored_response(notification) if self.udp is not None and "server" in self.conf: # Attempt to send off the UDP wake request. try: yield deferToThread( requests.post( self.conf["server"], data=urlencode(self.udp["data"]), cert=self.conf.get("cert"), timeout=self.conf.get("server_timeout", 3))) except Exception as exc: self.log.debug("Could not send UDP wake request: {exc}", exc=exc) returnValue(ret_val)
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 route_notification(self, notification, uaid_data): """Route a notification to an internal node, and store it if the node can't deliver immediately or is no longer a valid node """ # Determine if they're connected at the moment node_id = uaid_data.get("node_id") uaid = uaid_data["uaid"] router = self.db.router # Node_id is present, attempt delivery. # - Send Notification to node # - Success: Done, return 200 # - Error (Node busy): Jump to Save notification below # - Error (Client gone, node gone/dead): Clear node entry for user # - Both: Done, return 503 if node_id: result = None try: result = yield self._send_notification(uaid, node_id, notification) except (ConnectError, ConnectionClosed, ResponseFailed, CancelledError, PotentialDataLoss) as exc: self.metrics.increment("updates.client.host_gone") yield deferToThread(router.clear_node, uaid_data).addErrback(self._eat_db_err) if isinstance(exc, ConnectionRefusedError): # Occurs if an IP record is now used by some other node # in AWS or if the connection timesout. self.log.debug("Could not route message: {exc}", exc=exc) if result and result.code == 200: returnValue(self.delivered_response(notification)) # Save notification, node is not present or busy # - Save notification # - Success (older version): Done, return 202 # - Error (db error): Done, return 503 try: yield self._save_notification(uaid_data, notification) except ClientError as e: log_exception = (e.response["Error"]["Code"] != "ProvisionedThroughputExceededException") raise RouterException("Error saving to database", status_code=503, response_body="Retry Request", log_exception=log_exception, errno=201) # - Lookup client again to get latest node state after save. # - Success (node found): Notify node of new notification # - Success: Done, return 200 # - Error (no client): Done, return 202 # - Error (no node): Clear node entry # - Both: Done, return 202 # - Success (no node): Done, return 202 # - Error (db error): Done, return 202 # - Error (no client) : Done, return 404 try: uaid_data = yield deferToThread(router.get_uaid, uaid) except ClientError: returnValue(self.stored_response(notification)) except ItemNotFound: self.metrics.increment("updates.client.deleted") raise RouterException("User was deleted", status_code=410, response_body="Invalid UAID", log_exception=False, errno=105) # Verify there's a node_id in here, if not we're done node_id = uaid_data.get("node_id") if not node_id: returnValue(self.stored_response(notification)) try: result = yield self._send_notification_check(uaid, node_id) except (ConnectError, ConnectionClosed, ResponseFailed) as exc: self.metrics.increment("updates.client.host_gone") if isinstance(exc, ConnectionRefusedError): self.log.debug("Could not route message: {exc}", exc=exc) yield deferToThread( router.clear_node, uaid_data).addErrback(self._eat_db_err) returnValue(self.stored_response(notification)) if result.code == 200: returnValue(self.delivered_response(notification)) else: ret_val = self.stored_response(notification) returnValue(ret_val)