func apnsresToError(apnsres *common.APNSResult, psp *push.PushServiceProvider, dp *push.DeliveryPoint) push.PushError { var err push.PushError switch apnsres.Status { case 0: err = nil case 1: err = push.NewBadDeliveryPointWithDetails(dp, "Processing Error") case 2: err = push.NewBadDeliveryPointWithDetails(dp, "Missing Device Token") case 3: err = push.NewBadNotificationWithDetails("Missing topic") case 4: err = push.NewBadNotificationWithDetails("Missing payload") case 5: err = push.NewBadNotificationWithDetails("Invalid token size") case 6: err = push.NewBadNotificationWithDetails("Invalid topic size") case 7: err = push.NewBadNotificationWithDetails("Invalid payload size") case 8: // err = NewBadDeliveryPointWithDetails(req.dp, "Invalid Token") // This token is invalid, we should unsubscribe this device. err = push.NewUnsubscribeUpdate(psp, dp) default: err = push.NewErrorf("Unknown Error: %d", apnsres.Status) } return err }
// validateRawAPNSPayload tests that the client-provided JSON payload can be sent to APNS. // It converts it to bytes if it is, otherwise it returns a push.PushError. func validateRawAPNSPayload(payload string) ([]byte, push.PushError) { // https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1 var data map[string]interface{} err := json.Unmarshal([]byte(payload), &data) if data == nil { return nil, push.NewBadNotificationWithDetails(fmt.Sprintf("Could not parse payload: %v", err)) } aps, ok := data["aps"] if !ok { return nil, push.NewBadNotificationWithDetails("Payload missing aps") } aps_dict, ok := aps.(map[string]interface{}) if !ok { return nil, push.NewBadNotificationWithDetails("aps is not a dictionary") } if _, ok := aps_dict["alert"]; !ok { if content_available, ok := aps_dict["content-available"]; !ok || content_available != "1" { return nil, push.NewBadNotificationWithDetails("Missing alert and this is not a silent notification(content-available is not 1)") } } // TODO: Could optionally validate provided fields further according to documentation of the "The Notification Payload" section. // Creating a custom struct would make it simpler. // (E.g. body, action-loc-key, loc-key, loc-args, badge, sound, content-available, launch-image) return []byte(payload), nil }
func admSinglePush(psp *push.PushServiceProvider, dp *push.DeliveryPoint, data []byte, notif *push.Notification) (string, push.PushError) { client := &http.Client{} req, err := admNewRequest(psp, dp, data) if err != nil { return "", err } defer req.Body.Close() resp, httpErr := client.Do(req) if httpErr != nil { return "", push.NewErrorf("Failed to send adm push: %v", httpErr.Error()) } defer resp.Body.Close() id := resp.Header.Get("x-amzn-RequestId") if resp.StatusCode != 200 { if resp.StatusCode == 503 || resp.StatusCode == 500 || resp.StatusCode == 429 { // By default, we retry after one minute. retryAfter := resp.Header.Get("Retry-After") retrySecond := 60 if retryAfter != "" { var retryErr error retrySecond, retryErr = strconv.Atoi(retryAfter) if retryErr != nil { retrySecond = 60 } } retryDuration := time.Duration(retrySecond) * time.Second err = push.NewRetryError(psp, dp, notif, retryDuration) return id, err } body, ioErr := ioutil.ReadAll(resp.Body) if ioErr != nil { return "", push.NewErrorf("Failed to read adm response: %v", err) } var fail admPushFailResponse jsonErr := json.Unmarshal(body, &fail) if jsonErr != nil { return "", push.NewErrorf("%v: %v", resp.StatusCode, string(body)) } reason := strings.ToLower(fail.Reason) switch reason { case "messagetoolarge": err = push.NewBadNotificationWithDetails("MessageTooLarge") case "invalidregistrationid": err = push.NewBadDeliveryPointWithDetails(dp, "InvalidRegistrationId") case "accesstokenexpired": // retry would fix it. err = push.NewRetryError(psp, dp, notif, 10*time.Second) default: err = push.NewErrorf("%v: %v", resp.StatusCode, fail.Reason) } return "", err } return id, nil }
func toAPNSPayload(n *push.Notification) ([]byte, push.PushError) { // If "uniqush.payload.apns" is provided, then that will be used instead of the other POST parameters. if payloadJSON, ok := n.Data["uniqush.payload.apns"]; ok { bytes, err := validateRawAPNSPayload(payloadJSON) return bytes, err } payload := make(map[string]interface{}) aps := make(map[string]interface{}) alert := make(map[string]interface{}) for k, v := range n.Data { switch k { case "msg": alert["body"] = v case "action-loc-key": alert[k] = v case "loc-key": alert[k] = v case "loc-args": alert[k] = parseList(v) case "badge", "content-available": b, err := strconv.Atoi(v) if err != nil { continue } else { aps[k] = b } case "sound": aps["sound"] = v case "img": alert["launch-image"] = v case "id": continue case "expiry": continue case "ttl": continue default: if strings.HasPrefix(k, "uniqush.") { // keys beginning with "uniqush." are reserved by uniqush. continue } payload[k] = v } } aps["alert"] = alert payload["aps"] = aps j, err := common.MarshalJSONUnescaped(payload) if err != nil { return nil, push.NewErrorf("Failed to convert notification data to JSON: %v", err) } if len(j) > maxPayLoadSize { return nil, push.NewBadNotificationWithDetails("payload is too large") } return j, nil }
func notifToMessage(notif *push.Notification) (msg *admMessage, err push.PushError) { if notif == nil || len(notif.Data) == 0 { err = push.NewBadNotificationWithDetails("empty notification") return } msg = new(admMessage) msg.Data = make(map[string]string, len(notif.Data)) if msggroup, ok := notif.Data["msggroup"]; ok { msg.MsgGroup = msggroup } if rawTTL, ok := notif.Data["ttl"]; ok { ttl, err := strconv.ParseInt(rawTTL, 10, 64) if err == nil { msg.TTL = ttl } } if rawPayload, ok := notif.Data["uniqush.payload.adm"]; ok { jsonErr := json.Unmarshal([]byte(rawPayload), &(msg.Data)) if jsonErr != nil { err = push.NewBadNotificationWithDetails(fmt.Sprintf("invalid uniqush.payload.adm: %v", jsonErr)) return } } else { for k, v := range notif.Data { if k == "msggroup" || k == "ttl" { continue } if strings.HasPrefix(k, "uniqush.") { // keys beginning with "uniqush." are reserved by Uniqush. continue } msg.Data[k] = v } } if len(msg.Data) == 0 { err = push.NewBadNotificationWithDetails("empty notification") return } return }
// Push will read all of the delivery points to send to from dpQueue and send responses on resQueue before closing the channel. If the notification data is invalid, // it will send only one response. func (self *pushService) Push(psp *push.PushServiceProvider, dpQueue <-chan *push.DeliveryPoint, resQueue chan<- *push.PushResult, notif *push.Notification) { defer close(resQueue) // Profiling // self.updateCheckPoint("") var err push.PushError req := new(common.PushRequest) req.PSP = psp req.Payload, err = toAPNSPayload(notif) if err == nil && len(req.Payload) > self.requestProcessor.GetMaxPayloadSize() { err = push.NewBadNotificationWithDetails(fmt.Sprintf("payload is too large: %d > %d", len(req.Payload), self.requestProcessor.GetMaxPayloadSize())) } if err != nil { res := new(push.PushResult) res.Provider = psp res.Content = notif res.Err = push.NewErrorf("Failed to create push: %v", err) resQueue <- res for _ = range dpQueue { } return } unixNow := uint32(time.Now().Unix()) expiry := unixNow + 60*60 if ttlstr, ok := notif.Data["ttl"]; ok { ttl, err := strconv.ParseUint(ttlstr, 10, 32) if err == nil { expiry = unixNow + uint32(ttl) } } req.Expiry = expiry req.Devtokens = make([][]byte, 0, 10) dpList := make([]*push.DeliveryPoint, 0, 10) for dp := range dpQueue { res := new(push.PushResult) res.Destination = dp res.Provider = psp res.Content = notif devtoken, ok := dp.FixedData["devtoken"] if !ok { res.Err = push.NewBadDeliveryPointWithDetails(dp, "NoDevtoken") resQueue <- res continue } btoken, err := hex.DecodeString(devtoken) if err != nil { res.Err = push.NewBadDeliveryPointWithDetails(dp, err.Error()) resQueue <- res continue } req.Devtokens = append(req.Devtokens, btoken) dpList = append(dpList, dp) } n := len(req.Devtokens) lastId := self.getMessageIds(n) req.MaxMsgId = lastId req.DPList = dpList // We send this request object to be processed by pushMux goroutine, to send responses/errors back. errChan := make(chan push.PushError) resChan := make(chan *common.APNSResult, n) req.ErrChan = errChan req.ResChan = resChan self.requestProcessor.AddRequest(req) // errChan closed means the message(s) is/are sent successfully to the APNs. // However, we may have not yet receieved responses from APNS - those are sent on resChan for err = range errChan { res := new(push.PushResult) res.Provider = psp res.Content = notif if _, ok := err.(*push.ErrorReport); ok { res.Err = push.NewErrorf("Failed to send payload to APNS: %v", err) } else { res.Err = err } resQueue <- res } // Profiling // self.updateCheckPoint("sending the message takes") if err != nil { return } for i, dp := range dpList { if dp != nil { r := new(push.PushResult) r.Provider = psp r.Content = notif r.Destination = dp mid := req.GetId(i) r.MsgId = fmt.Sprintf("apns:%v-%v", psp.Name(), mid) r.Err = nil resQueue <- r } } // Wait for the unserialized responses from APNS asynchronously - these will not affect what we send our clients for this request, but will affect subsequent requests. go self.waitResults(psp, dpList, lastId, resChan) }