Ejemplo n.º 1
0
    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
Ejemplo n.º 3
0
    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,
        )
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
 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
Ejemplo n.º 6
0
 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
Ejemplo n.º 7
0
 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)
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
    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)
Ejemplo n.º 10
0
 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)
Ejemplo n.º 11
0
 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))
Ejemplo n.º 12
0
 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)
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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)
Ejemplo n.º 15
0
    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)
Ejemplo n.º 16
0
 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")
Ejemplo n.º 17
0
    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, "")
Ejemplo n.º 18
0
    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
Ejemplo n.º 19
0
    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
Ejemplo n.º 20
0
 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
Ejemplo n.º 21
0
    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, "")
Ejemplo n.º 22
0
    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
Ejemplo n.º 23
0
    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)
Ejemplo n.º 24
0
    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)
Ejemplo n.º 25
0
    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)
Ejemplo n.º 26
0
    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)
Ejemplo n.º 27
0
    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)
Ejemplo n.º 28
0
    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)
Ejemplo n.º 29
0
    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)
Ejemplo n.º 30
0
    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)