Example #1
0
    def dispatchNotification(self, n):
        pushkeys = [device.pushkey for device in n.devices if device.app_id == self.name]
        # Resolve canonical IDs for all pushkeys
        pushkeys = [canonical_reg_id or reg_id for (reg_id, canonical_reg_id) in
                    self.canonical_reg_id_store.get_canonical_ids(pushkeys).items()]

        data = GcmPushkin.build_data(n)

        # TODO: Implement collapse_key to queue only one message per room.
        request = gcmclient.JSONMessage(pushkeys, data)
        failed = []

        logger.info("%r => %r", data, pushkeys);

        for retry in range(0, MAX_TRIES):
            response = self.gcm.send(request)

            for reg_id, msg_id in response.success.items():
                logger.debug(
                    "Successfully sent notification %s to %s as %s",
                    n.id, reg_id, msg_id)

            for reg_id, canonical_reg_id in response.canonical.items():
                self.canonical_reg_id_store.set_canonical_id(reg_id, canonical_reg_id)

            # gcm-client extracts the NotRegistered errors and puts the reg_ids in
            # the not_registered list.
            failed.extend(response.not_registered)
            logger.info("Reg IDs Not Registered: %r", response.not_registered);
            # Other errors live in the `failed` dict, but some of those mean
            # the reg_id is permanently dead too and we should remove it.
            for failed_reg_id,error_code in response.failed.items():
                if error_code in PERMANENT_FAILURE_CODES:
                    logger.info(
                        "Reg ID %r has permanently failed with code %r",
                        failed_reg_id, error_code
                    )
                    failed.append(failed_reg_id)
                else:
                    logger.info(
                        "Reg ID %r has temporarily failed with code %r",
                        failed_reg_id, error_code
                    )

            if not response.needs_retry():
                break

            request = response.retry()
            gevent.wait(timeout=response.delay(retry))
        else:
            # response.unavailable is a list of reg IDs that failed temporarily
            # but is undocumented in gcmclient's API
            logger.info("Gave up retrying reg IDs: %r", response.unavailable);

        return failed
Example #2
0
def send_android_push_notification(user, data):
    # type: (UserProfile, Dict[str, Any]) -> None
    if not gcm:
        logging.error("Attempting to send a GCM push notification, but no API key was configured")
        return

    reg_ids = [device.token for device in
        PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.GCM)]

    msg = gcmclient.JSONMessage(reg_ids, data)
    res = gcm.send(msg)

    for reg_id, msg_id in res.success.items():
        logging.info("GCM: Sent %s as %s" % (reg_id, msg_id))

    # res.canonical will contain results when there are duplicate registrations for the same
    # device. The "canonical" registration is the latest registration made by the device.
    # Ref: http://developer.android.com/google/gcm/adv.html#canonical
    for reg_id, new_reg_id in res.canonical.items():
        if reg_id == new_reg_id:
            # I'm not sure if this should happen. In any case, not really actionable.
            logging.warning("GCM: Got canonical ref but it already matches our ID %s!" % (reg_id,))
        elif not PushDeviceToken.objects.filter(token=new_reg_id, kind=PushDeviceToken.GCM).count():
            # This case shouldn't happen; any time we get a canonical ref it should have been
            # previously registered in our system.
            #
            # That said, recovery is easy: just update the current PDT object to use the new ID.
            logging.warning(
                    "GCM: Got canonical ref %s replacing %s but new ID not registered! Updating." %
                    (new_reg_id, reg_id))
            PushDeviceToken.objects.filter(
                    token=reg_id, kind=PushDeviceToken.GCM).update(token=new_reg_id)
        else:
            # Since we know the new ID is registered in our system we can just drop the old one.
            logging.info("GCM: Got canonical ref %s, dropping %s" % (new_reg_id, reg_id))

            PushDeviceToken.objects.filter(token=reg_id, kind=PushDeviceToken.GCM).delete()

    for reg_id in res.not_registered:
        logging.info("GCM: Removing %s" % (reg_id,))

        device = PushDeviceToken.objects.get(token=reg_id, kind=PushDeviceToken.GCM)
        device.delete()

    for reg_id, err_code in res.failed.items():
        logging.warning("GCM: Delivery to %s failed: %s" % (reg_id, err_code))

    if res.needs_retry():
        # TODO
        logging.warning("GCM: delivery needs a retry but ignoring")
Example #3
0
    def _route(self, notification, router_data):
        """Blocking GCM call to route the notification"""
        data = {"chid": notification.channel_id}
        # 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 len(notification.data) > 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" %
                                  (len(notification.data) - mdata),
                                  413,
                                  errno=104)

            data['body'] = notification.data
            data['con'] = notification.headers['content-encoding']
            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 GCM instance tokens (specified during
        # registration.
        router_ttl = notification.ttl or 0
        payload = gcmclient.JSONMessage(
            registration_ids=[router_data.get("token")],
            collapse_key=self.collapseKey,
            time_to_live=max(self.min_ttl, router_ttl),
            dry_run=self.dryRun or ("dryrun" in router_data),
            data=data,
        )
        creds = router_data.get("creds", {"senderID": "missing id"})
        try:
            self.gcm.api_key = creds["auth"]
            result = self.gcm.send(payload)
        except KeyError:
            raise self._error(
                "Server error, missing bridge credentials " +
                "for %s" % creds.get("senderID"), 500)
        except gcmclient.GCMAuthenticationError, e:
            raise self._error("Authentication Error: %s" % e, 500)
Example #4
0
 def _route(self, notification, router_data):
     """Blocking GCM call to route the notification"""
     payload = gcmclient.JSONMessage(
         registration_ids=[router_data["token"]],
         collapse_key=self.collapseKey,
         time_to_live=self.ttl,
         dry_run=self.dryRun,
         data={"Msg": notification.data,
               "Chid": notification.channel_id,
               "Ver": notification.version}
     )
     creds = router_data.get("creds", {"senderID": "missing id"})
     try:
         self.gcm.api_key = creds["auth"]
         result = self.gcm.send(payload)
     except KeyError:
         self._error("Server error, missing bridge credentials for %s" %
                     creds.get("senderID"), 500)
     except gcmclient.GCMAuthenticationError, e:
         self._error("Authentication Error: %s" % e, 500)
Example #5
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.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.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 KeyError:
            self.log.critical("Missing GCM bridge credentials")
            raise RouterException("Server error", status_code=500)
        except gcmclient.GCMAuthenticationError as e:
            self.log.error("GCM Authentication Error: %s" % e)
            raise RouterException("Server error", status_code=500)
        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,
                                  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)