Skip to content
This repository has been archived by the owner on Mar 28, 2019. It is now read-only.

Commit

Permalink
Fixed to use v2 APNS messaging.
Browse files Browse the repository at this point in the history
  • Loading branch information
jrconlin committed Mar 3, 2015
1 parent 01cdd5c commit e4dd72c
Showing 1 changed file with 193 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"fmt"
"io"
"net"
"strings"
"time"

"github.com/mozilla-services/pushgo/retry"
Expand Down Expand Up @@ -54,6 +55,8 @@ type APNSPing struct {
closeOnce Once
closeSignal chan bool
conn *APNSSocket
title string
body string
}

type APNSPingConfig struct {
Expand All @@ -63,19 +66,47 @@ type APNSPingConfig struct {
CertFile string `toml:"certfile" env:"apns_certfile"`
KeyFile string `toml:"keyfile" env:"apns_keyfile"`
Retry retry.Config
Title string `toml:"default_title" env:"apns_default_title"`
Body string `toml:"default_body" env:"apns_default_body"`
}

type APNSPingData struct {
Version int64 `json:"version"`
Data string `json:"data",omitempty`
Version int64
Data string
Title string
Body string
}

/*
// Future use
type APNSAlert struct {
Title string `json:"title"`
Body string `json:"body"`
TitleLocKey string `json:"title-loc-key"`
TitleLocArgs []string `json:"title-loc-args"`
ActionLocKey string `json:"action-loc-key"`
LocKey string `json:"loc-key"`
LocArgs []string `json:"loc-args"`
LaunchImage string `json:"launch-image"`
}
*/

type APNSAlert struct {
// Alert is either an APNSAlert or a string containing the alert body
//Alert *APNSAlert `json:"alert"`
Title string `json:"title"`
Body string `json:"body"`
Badge uint64 `json:"badge,omitempty"`
Sound string `json:"sound,omitempty"`
ContentAvailable uint8 `json:"content-available"`
}

type APNSPayload struct {
Alert *APNSPingData `json:"alert"`
Badge uint64 `json:"badge,omitempty"`
Sound string `json:"sound,omitempty"`
ContentAvailable uint8 `json:"content-available"`
APS *APNSAlert `json:"aps"`
Version int64 `json:"version"`
Data string `json:"data",omitempty"`
}

type APNSStatus struct {
Command uint8
Status uint8
Expand All @@ -93,9 +124,14 @@ const (
var (
ErrAPNSPayloadTooLarge = errors.New("Payload Data limited to less than 2048K")
ErrAPNSUnavailable = errors.New("APNS is unavailable")
notifNum = uint64(1)
)

func (r *APNSSocket) Dial(addr string) (err error) {
if addr == "" {
addr = r.addr
}
fmt.Printf("!!! Trying %s %s\n", r.CertFile, r.KeyFile)
cert, err := tls.LoadX509KeyPair(r.CertFile, r.KeyFile)
if err != nil {
return
Expand All @@ -115,6 +151,9 @@ func (r *APNSSocket) Dial(addr string) (err error) {
return
}
}
if r.addr == "" {
r.addr = addr
}
r.tlsconn = tls.Client(r.conn, tlsConf)
err = r.tlsconn.Handshake()
if err != nil {
Expand All @@ -123,6 +162,90 @@ func (r *APNSSocket) Dial(addr string) (err error) {
}

func (r *APNSSocket) Send(token string, data *APNSPingData) (err error) {
btoken, err := hex.DecodeString(token)
if err != nil {
return err
}
payload := &APNSPayload{
APS: &APNSAlert{
Title: data.Title,
Body: data.Body,
ContentAvailable: 1,
Sound: "default",
},
Version: data.Version,
Data: data.Data,
}
payloadBody, err := json.Marshal(payload)
fmt.Printf("\n>> Payload:\n%s\n", payloadBody)
if err != nil {
return err
}
if int64(len(payloadBody)) > APNS_MAX_PAYLOAD_LEN {
return ErrAPNSPayloadTooLarge
}

// TODO: Wrap with retry logic.
// Build item
item := bytes.NewBuffer([]byte{})
// Device Token
binary.Write(item, binary.BigEndian, uint8(0))
binary.Write(item, binary.BigEndian, uint16(len(btoken)))
binary.Write(item, binary.BigEndian, btoken)
// Payload
binary.Write(item, binary.BigEndian, uint16(len(payloadBody)))
binary.Write(item, binary.BigEndian, payloadBody)
// try sending...
fmt.Printf("\n >>>> Sending APNS #%d to %s[%s]\n%v\n", notifNum, r.addr, token, item.Bytes())
notifNum++
for retry := 2; retry != 0; retry-- {
_, err = r.tlsconn.Write(item.Bytes())
if err != nil {
fmt.Printf("\n !!! Got Error %+v %T\n", err, err)
fmt.Printf("\n\n### Redialing to %s\n", r.addr)
err = r.Dial(r.addr)
if err != nil {
fmt.Printf("\n\n### Dial failed...%s\n", err.Error)
return
}
fmt.Printf("\n\n### Retrying send...\n")
continue
}
fmt.Printf("\n >>>> Success!\n")
err = nil
break
}
// If we hear nothing, all went well.
// which, kinda sucks, because it means we have no idea until the
// connection times out.
r.tlsconn.SetReadDeadline(time.Now().Add(5 * time.Second))
reply := make([]byte, 6)
fmt.Printf("\n>>>> Checking read queue\n")
n, err := r.tlsconn.Read(reply[:])
if err != nil && err != io.EOF {
fmt.Printf("\n >>>> Read failed %+v\n", err)
return
}
if n != 0 {
status := &APNSStatus{}
binary.Read(bytes.NewBuffer(reply), binary.BigEndian, &status)
if status.Status != 0 {
fmt.Printf(">>>> Read returned: %v\n", reply)
msg := fmt.Sprintf("Resend request: code %d, ID %x",
status.Status,
status.Identifier)
return errors.New(msg)
}
fmt.Printf("\nSuccess:: %+v [%v]\n", status, reply)
} else {
fmt.Printf(" --- Reader returned 0 length\n")
}
fmt.Printf("\n ==== Done \n")
return nil
}

// Use v2 of send (which apparently doesn't work according to the docs)
func (r *APNSSocket) Send2(token string, data *APNSPingData) (err error) {
// Convert the data block into a datastream
if int64(len(data.Data)) > APNS_MAX_PAYLOAD_LEN {
return ErrAPNSPayloadTooLarge
Expand All @@ -132,11 +255,16 @@ func (r *APNSSocket) Send(token string, data *APNSPingData) (err error) {
return err
}
payload := &APNSPayload{
Alert: data,
//Specify "Content Available = 1" for silent alerts
ContentAvailable: 1,
APS: &APNSAlert{
Title: data.Title,
Body: data.Body,
ContentAvailable: 1,
},
Version: data.Version,
Data: data.Data,
}
payloadBody, err := json.Marshal(payload)
fmt.Printf("\n>> Payload:\n%s\n", payloadBody)
if err != nil {
return err
}
Expand All @@ -155,19 +283,19 @@ func (r *APNSSocket) Send(token string, data *APNSPingData) (err error) {
binary.Write(item, binary.BigEndian, uint8(2))
binary.Write(item, binary.BigEndian, uint16(len(payloadBody)))
binary.Write(item, binary.BigEndian, payloadBody)
// Notification ID (currently, just "1")
// Notification ID
binary.Write(item, binary.BigEndian, uint8(3))
binary.Write(item, binary.BigEndian, uint16(1))
binary.Write(item, binary.BigEndian, 1)
binary.Write(item, binary.BigEndian, uint16(4))
binary.Write(item, binary.BigEndian, uint32(notifNum))
// Expiration Date (currently now + 3 days)
exp := time.Now().UTC().Add(time.Hour * 72).Unix()
exp := time.Now().UTC().Add(time.Minute * 5).Unix()
binary.Write(item, binary.BigEndian, uint8(4))
binary.Write(item, binary.BigEndian, uint16(4))
binary.Write(item, binary.BigEndian, uint32(exp))
// Priority
binary.Write(item, binary.BigEndian, uint8(5))
binary.Write(item, binary.BigEndian, uint16(1))
binary.Write(item, binary.BigEndian, APNS_SEND_IMMEDIATE)
binary.Write(item, binary.BigEndian, uint8(APNS_SEND_IMMEDIATE))

// Build the wrapper frame
frame := bytes.NewBuffer([]byte{})
Expand All @@ -177,31 +305,52 @@ func (r *APNSSocket) Send(token string, data *APNSPingData) (err error) {
binary.Write(frame, binary.BigEndian, item.Bytes())

// try sending...
_, err = r.tlsconn.Write(frame.Bytes())
if err != nil {
return
fmt.Printf("\n >>>> Sending APNS #%d to %s[%s]\n%x\n", notifNum, r.addr, token, frame.Bytes())
notifNum++
for retry := 2; retry != 0; retry-- {
_, err = r.tlsconn.Write(frame.Bytes())
if err != nil {
fmt.Printf("\n !!! Got Error %+v %T\n", err, err)
fmt.Printf("\n\n### Redialing to %s\n", r.addr)
err = r.Dial(r.addr)
if err != nil {
fmt.Printf("\n\n### Dial failed...%s\n", err.Error)
return
}
fmt.Printf("\n\n### Retrying send...\n")
continue
}
fmt.Printf("\n >>>> Success!\n")
err = nil
break
}
// If we hear nothing, all went well.
// which, kinda sucks, because it means we have no idea until the
// connection times out.
r.tlsconn.SetReadDeadline(time.Now().Add(5 * time.Second))
r.tlsconn.SetReadDeadline(time.Now().Add(r.Timeout))
reply := make([]byte, 6)
fmt.Printf("\n>>>> Checking read queue\n")
n, err := r.tlsconn.Read(reply[:])
if err != nil && err != io.EOF {
if err != nil && !strings.Contains(err.Error(), "i/o timeout") {
fmt.Printf("\n >>>> Read failed %+v\n", err)
return
}
if n != 0 {
status := &APNSStatus{}
binary.Read(bytes.NewBuffer(reply), binary.BigEndian, &status)
if status.Status != 0 {
fmt.Printf(">>>> Read returned: %v\n", reply)
msg := fmt.Sprintf("Resend request: code %d, ID %x",
status.Status,
status.Identifier)
return errors.New(msg)
}
err = nil
fmt.Printf("\nSuccess:: %+v [%v]\n", status, reply)
} else {
fmt.Printf(" --- Reader returned 0 length\n")
}
return
fmt.Printf("\n ==== Done \n")
return nil
}

func (r *APNSSocket) Close() {
Expand All @@ -224,6 +373,8 @@ func (r *APNSPing) ConfigStruct() interface{} {
CertFile: "apns.cert",
KeyFile: "apns.key",
Timeout: "3s",
Title: "A new Push Event happened.",
Body: "I wish that silent pushes actually worked.",
Retry: retry.Config{
Retries: 5,
Delay: "200ms",
Expand All @@ -241,6 +392,8 @@ func (r *APNSPing) Init(app *Application, config interface{}) (err error) {

r.host = conf.Host
r.port = conf.Port
r.title = conf.Title
r.body = conf.Body
timeout, err := time.ParseDuration(conf.Timeout)
if err != nil {
r.logger.Error("propping", "Could not parse timeout, using default",
Expand Down Expand Up @@ -281,6 +434,8 @@ func (r *APNSPing) CanBypassWebsocket() bool {
type apnsConnectData struct {
Type string `json:"type,omitempty"`
Token string `json:"token"`
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
}

func (r *APNSPing) Register(uaid string, pingData []byte) (err error) {
Expand All @@ -298,7 +453,7 @@ func (r *APNSPing) Register(uaid string, pingData []byte) (err error) {
}
return err
}
if err = r.store.PutPing(uaid, []byte(ping.Token)); err != nil {
if err = r.store.PutPing(uaid, pingData); err != nil {
if r.logger.ShouldLog(ERROR) {
r.logger.Error("propping", "Could not store APNS registration data",
LogFields{"error": err.Error()})
Expand All @@ -317,29 +472,39 @@ func (r *APNSPing) Send(uaid string, vers int64, data string) (ok bool, err erro
return false, ErrAPNSUnavailable
}

token, err := r.store.FetchPing(uaid)
pingData := &apnsConnectData{}
pingStore, err := r.store.FetchPing(uaid)
if err != nil {
if r.logger.ShouldLog(ERROR) {
r.logger.Error("propping", "Could not fetch APNS registration data",
LogFields{"error": err.Error(), "uaid": uaid})
r.logger.Error("propping", "Could not fetch ping data for device",
LogFields{"uaid": uaid, "error": err.Error()})
}
return false, err
}
if len(token) == 0 {
if len(pingStore) == 0 {
if r.logger.ShouldLog(INFO) {
r.logger.Info("propping", "No APNS registration data for device",
LogFields{"uaid": uaid})
}
return false, nil
}
err = json.Unmarshal(pingStore, pingData)
if err != nil {
if r.logger.ShouldLog(ERROR) {
r.logger.Error("propping", "Could not fetch APNS registration data",
LogFields{"error": err.Error(), "uaid": uaid})
}
return false, err
}
if r.conn == nil {
return false, ErrAPNSUnavailable
}
ping := &APNSPingData{
Title: pingData.Title,
Body: pingData.Body,
Version: vers,
Data: data,
}
err = r.conn.Send(string(token), ping)
err = r.conn.Send2(string(pingData.Token), ping)
if err != nil {
if r.logger.ShouldLog(WARNING) {
r.logger.Warn("proppring", "Could not send APNS ping",
Expand Down

0 comments on commit e4dd72c

Please sign in to comment.