Repository: timehop/apns Branch: master Commit: c2b19f701c60 Files: 21 Total size: 69.5 KB Directory structure: gitextract_u9iwx99q/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── apns_suite_test.go ├── badge_number.go ├── badge_number_test.go ├── client.go ├── client_test.go ├── conn.go ├── conn_test.go ├── doc.go ├── error.go ├── error_test.go ├── example/ │ └── example.go ├── feedback.go ├── feedback_test.go ├── go.mod ├── go.sum ├── notification.go └── notification_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test # Example keys example/*.key example/*.crt # Ginkgo files *.coverprofile coverage.html ================================================ FILE: .travis.yml ================================================ language: go go: - 1.3 services: - redis-server before_script: - go get github.com/onsi/ginkgo - go get github.com/onsi/gomega - go get code.google.com/p/go.tools/cmd/cover - go install github.com/onsi/ginkgo/ginkgo script: ginkgo -r --skipMeasurements --cover --trace env: global: - PATH=$HOME/gopath/bin:$PATH ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 timehop Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # apns [![GoDoc](https://godoc.org/github.com/timehop/apns?status.svg)](https://godoc.org/github.com/timehop/apns) [![Build Status](https://travis-ci.org/timehop/apns.svg?branch=master)](https://travis-ci.org/timehop/apns) A Go package to interface with the Apple Push Notification Service ## Features This library implements a few features that we couldn't find in any one library elsewhere: * **Long Lived Clients** - Apple's documentation say that you should hold [a persistent connection open](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW6) and not create new connections for every payload * **Use of New Protocol** - Apple came out with v2 of their API with support for variable length payloads. This library uses that protocol. * **Robust Send Guarantees** - APNS has asynchronous feedback on whether a push sent. That means that if you send pushes after a bad send, those pushes will be lost forever. Our library records the last N pushes, detects errors, and is able to resend the pushes that could have been lost. [More reading](http://redth.codes/the-problem-with-apples-push-notification-ser/) ## API Compatibility The apns package may undergo breaking changes. A tool like [godep](https://github.com/tools/godep) is recommended to vendor the current release. ## Install ``` go get github.com/timehop/apns ``` Checkout the `develop` branch for the current work in progress. ## Usage ### Sending a push notification (basic) ```go c, _ := apns.NewClient(apns.ProductionGateway, apnsCert, apnsKey) p := apns.NewPayload() p.APS.Alert.Body = "I am a push notification!" p.APS.Badge.Set(5) p.APS.Sound = "turn_down_for_what.aiff" m := apns.NewNotification() m.Payload = p m.DeviceToken = "A_DEVICE_TOKEN" m.Priority = apns.PriorityImmediate c.Send(m) ``` ### Sending a push notification with error handling ```go c, err := apns.NewClientWithFiles(apns.ProductionGateway, "cert.pem", "key.pem") if err != nil { log.Fatal("could not create new client", err.Error()) } go func() { for f := range c.FailedNotifs { fmt.Println("Notif", f.Notif.ID, "failed with", f.Err.Error()) } }() p := apns.NewPayload() p.APS.Alert.Body = "I am a push notification!" p.APS.Badge.Set(5) p.APS.Sound = "turn_down_for_what.aiff" p.APS.ContentAvailable = 1 p.SetCustomValue("link", "zombo://dot/com") p.SetCustomValue("game", map[string]int{"score": 234}) m := apns.NewNotification() m.Payload = p m.DeviceToken = "A_DEVICE_TOKEN" m.Priority = apns.PriorityImmediate m.Identifier = 12312 // Integer for APNS m.ID = "user_id:timestamp" // ID not sent to Apple – to identify error notifications c.Send(m) ``` ### Retrieving feedback ```go f, err := apns.NewFeedback(s.Address(), DummyCert, DummyKey) if err != nil { log.Fatal("Could not create feedback", err.Error()) } for ft := range f.Receive() { fmt.Println("Feedback for token:", ft.DeviceToken) } ``` Note that the channel returned from `Receive` will close after the [feedback service](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3) has no more data to send. ## Running the tests We use [Ginkgo](https://onsi.github.io/ginkgo) for our testing framework and [Gomega](http://onsi.github.io/gomega/) for our matchers. To run the tests: ``` go get github.com/onsi/ginkgo/ginkgo go get github.com/onsi/gomega ginkgo -randomizeAllSpecs ``` ## Contributing - Fork the repo ([Recommended process](https://splice.com/blog/contributing-open-source-git-repositories-go/)) - Make your changes - [Run the tests](https://github.com/timehop/apns#running-the-tests) - Submit a pull request ## License [MIT License](https://github.com/timehop/apns/blob/master/LICENSE) ================================================ FILE: apns_suite_test.go ================================================ package apns_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) func TestApns(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Apns Suite") } ================================================ FILE: badge_number.go ================================================ package apns import "encoding/json" // BadgeNumber is much a NullInt64 in // database/sql except instead of using // the nullable value for driver.Value // encoding and decoding, this is specifically // meant for JSON encoding and decoding type BadgeNumber struct { Number uint IsSet bool } // Unset will reset the BadgeNumber to // both 0 and invalid func (b *BadgeNumber) Unset() { b.Number = 0 b.IsSet = false } // Set will set the BadgeNumber value to // the number passed in, assuming it's >= 0. // If so, the BadgeNumber will also be marked valid func (b *BadgeNumber) Set(number uint) { b.Number = number b.IsSet = true } // MarshalJSON will marshall the numerical value of // BadgeNumber func (b BadgeNumber) MarshalJSON() ([]byte, error) { return json.Marshal(b.Number) } // UnmarshalJSON will take any non-nil value and // set BadgeNumber's numeric value to it. It assumes // that if the unmarshaller gets here, there is a // number to unmarshal and it's valid func (b *BadgeNumber) UnmarshalJSON(data []byte) error { err := json.Unmarshal(data, &b.Number) if err != nil { return err } // Since the point of this type is to // allow proper inclusion of 0 for int // types while respecting omitempty, // assume that set==true if there is // a value to unmarshal b.IsSet = true return nil } ================================================ FILE: badge_number_test.go ================================================ package apns_test import ( "encoding/json" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/timehop/apns" ) var _ = Describe("BadgeNumber", func() { Describe("Defaults", func() { It("Should have the proper default values", func() { b := apns.BadgeNumber{} Expect(b.Number).To(Equal(uint(0))) Expect(b.IsSet).To(BeFalse()) }) }) Describe("NewBadgeNumber", func() { var b apns.BadgeNumber Context("with an argument", func() { It("should have values set properly", func() { b.Set(5) Expect(b.IsSet).To(BeTrue()) Expect(b.Number).To(Equal(uint(5))) }) }) Context("when unset", func() { It("should reset its values", func() { b.Unset() Expect(b.IsSet).To(BeFalse()) Expect(b.Number).To(Equal(uint(0))) }) }) }) Describe("JSON handling", func() { type BadgeNumbers struct { A apns.BadgeNumber `json:"a"` B apns.BadgeNumber `json:"b"` } Context("when marshalling", func() { It("should not error", func() { bn := apns.BadgeNumber{} bn.Set(10) _, err := json.Marshal(BadgeNumbers{ A: bn, }) Expect(err).To(BeNil()) }) It("should create the proper values", func() { bn := apns.BadgeNumber{} bn.Set(10) bns := BadgeNumbers{ A: bn, } b, _ := json.Marshal(bns) expected := "{\"a\":10,\"b\":0}" Expect(string(b)).To(Equal(expected)) }) }) Context("when unmarshalled", func() { var bnumbers BadgeNumbers It("should unmarshal without errors", func() { err := json.Unmarshal([]byte("{\"a\":10,\"b\":0}"), &bnumbers) Expect(err).To(BeNil()) }) It("should populate the struct properly", func() { json.Unmarshal([]byte("{\"a\":10,\"b\":0}"), &bnumbers) Expect(bnumbers.A.IsSet).To(BeTrue()) Expect(bnumbers.B.IsSet).To(BeTrue()) Expect(bnumbers.A.Number).To(Equal(uint(10))) Expect(bnumbers.B.Number).To(Equal(uint(0))) }) }) }) }) ================================================ FILE: client.go ================================================ package apns import ( "container/list" "crypto/tls" "io" "log" "time" ) type buffer struct { size int *list.List } func newBuffer(size int) *buffer { return &buffer{size, list.New()} } func (b *buffer) Add(v interface{}) *list.Element { e := b.PushBack(v) if b.Len() > b.size { b.Remove(b.Front()) } return e } type Client struct { Conn *Conn FailedNotifs chan NotificationResult notifs chan Notification id uint32 } func newClientWithConn(gw string, conn Conn) Client { c := Client{ Conn: &conn, FailedNotifs: make(chan NotificationResult), id: uint32(1), notifs: make(chan Notification), } go c.runLoop() return c } func NewClientWithCert(gw string, cert tls.Certificate) Client { conn := NewConnWithCert(gw, cert) return newClientWithConn(gw, conn) } func NewClient(gw string, cert string, key string) (Client, error) { conn, err := NewConn(gw, cert, key) if err != nil { return Client{}, err } return newClientWithConn(gw, conn), nil } func NewClientWithFiles(gw string, certFile string, keyFile string) (Client, error) { conn, err := NewConnWithFiles(gw, certFile, keyFile) if err != nil { return Client{}, err } return newClientWithConn(gw, conn), nil } func (c *Client) Send(n Notification) error { c.notifs <- n return nil } func (c *Client) reportFailedPush(v interface{}, err *Error) { failedNotif, ok := v.(Notification) if !ok || v == nil { return } select { case c.FailedNotifs <- NotificationResult{Notif: failedNotif, Err: *err}: default: } } func (c *Client) requeue(cursor *list.Element) { // If `cursor` is not nil, this means there are notifications that // need to be delivered (or redelivered) for ; cursor != nil; cursor = cursor.Next() { if n, ok := cursor.Value.(Notification); ok { go func() { c.notifs <- n }() } } } func (c *Client) handleError(err *Error, buffer *buffer) *list.Element { cursor := buffer.Back() for cursor != nil { // Get notification n, _ := cursor.Value.(Notification) // If the notification, move cursor after the trouble notification if n.Identifier == err.Identifier { go c.reportFailedPush(cursor.Value, err) next := cursor.Next() buffer.Remove(cursor) return next } cursor = cursor.Prev() } return cursor } func (c *Client) runLoop() { sent := newBuffer(50) cursor := sent.Front() // APNS connection for { err := c.Conn.Connect() if err != nil { // TODO Probably want to exponentially backoff... time.Sleep(1 * time.Second) continue } // Start reading errors from APNS errs := readErrs(c.Conn) c.requeue(cursor) // Connection open, listen for notifs and errors for { var err error var n Notification // Check for notifications or errors. There is a chance we'll send notifications // if we already have an error since `select` will "pseudorandomly" choose a // ready channels. It turns out to be fine because the connection will already // be closed and it'll requeue. We could check before we get to this select // block, but it doesn't seem worth the extra code and complexity. select { case err = <-errs: case n = <-c.notifs: } // If there is an error we understand, find the notification that failed, // move the cursor right after it. if nErr, ok := err.(*Error); ok { cursor = c.handleError(nErr, sent) break } if err != nil { break } // Add to list cursor = sent.Add(n) // Set identifier if not specified if n.Identifier == 0 { n.Identifier = c.id c.id++ } else if c.id < n.Identifier { c.id = n.Identifier + 1 } b, err := n.ToBinary() if err != nil { // TODO continue } _, err = c.Conn.Write(b) if err == io.EOF { log.Println("EOF trying to write notification") break } if err != nil { log.Println("err writing to apns", err.Error()) break } cursor = cursor.Next() } } } func readErrs(c *Conn) chan error { errs := make(chan error) go func() { p := make([]byte, 6, 6) _, err := c.Read(p) if err != nil { errs <- err return } e := NewError(p) errs <- &e }() return errs } ================================================ FILE: client_test.go ================================================ package apns_test import ( "bytes" "encoding/binary" "io/ioutil" "os" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/timehop/apns" ) var _ = Describe("Client", func() { Describe(".NewConn", func() { Context("bad cert/key pair", func() { It("should error out", func() { _, err := apns.NewClient(apns.ProductionGateway, "missing", "missing_also") Expect(err).NotTo(BeNil()) }) }) Context("valid cert/key pair", func() { It("should create a valid client", func() { c, err := apns.NewClient(apns.ProductionGateway, DummyCert, DummyKey) Expect(err).To(BeNil()) Expect(c.Conn).NotTo(BeNil()) }) }) }) Describe(".NewConnWithFiles", func() { Context("missing cert/key pair", func() { It("should error out", func() { _, err := apns.NewClientWithFiles(apns.ProductionGateway, "missing", "missing_also") Expect(err).NotTo(BeNil()) }) }) Context("valid cert/key pair", func() { var certFile, keyFile *os.File BeforeEach(func() { certFile, _ = ioutil.TempFile("", "cert.pem") certFile.Write([]byte(DummyCert)) certFile.Close() keyFile, _ = ioutil.TempFile("", "key.pem") keyFile.Write([]byte(DummyKey)) keyFile.Close() }) AfterEach(func() { if certFile != nil { os.Remove(certFile.Name()) } if keyFile != nil { os.Remove(keyFile.Name()) } }) It("should create a valid client", func() { c, err := apns.NewClientWithFiles(apns.ProductionGateway, certFile.Name(), keyFile.Name()) Expect(err).To(BeNil()) Expect(c.Conn).NotTo(BeNil()) }) }) }) Describe("#Send", func() { Context("simple write", func() { as := [][]serverAction{ []serverAction{ serverAction{action: readAction, data: []byte{}}, }, } It("should not return an error", func(d Done) { mockDone := make(chan interface{}) withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true Expect(c.Send(apns.Notification{})).To(BeNil()) close(mockDone) close(d) }) }) }) Context("simple write with buffer", func() { as := [][]serverAction{ []serverAction{ serverAction{action: readAction, data: []byte{}}, }, } It("should not return an error", func(d Done) { mockDone := make(chan interface{}) withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true for i := 0; i < 54; i++ { Expect(c.Send(apns.Notification{})).To(BeNil()) } close(mockDone) close(d) }) }) }) Context("multiple write", func() { as := [][]serverAction{ []serverAction{ serverAction{action: readAction, data: []byte{}}, serverAction{action: readAction, data: []byte{}}, }, } It("should not return an error", func(d Done) { mockDone := make(chan interface{}) withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true Expect(c.Send(apns.Notification{})).To(BeNil()) Expect(c.Send(apns.Notification{})).To(BeNil()) close(mockDone) close(d) }) }) }) Context("bad push", func() { n := apns.Notification{Identifier: 9, ID: "some_rando"} nb, _ := n.ToBinary() nbcb := make([]byte, len(nb)) errPayload := bytes.NewBuffer([]byte{}) binary.Write(errPayload, binary.BigEndian, uint8(8)) binary.Write(errPayload, binary.BigEndian, uint8(8)) binary.Write(errPayload, binary.BigEndian, uint32(9)) as := [][]serverAction{ []serverAction{ serverAction{action: readAction, data: []byte{}}, serverAction{action: readAction, data: nbcb, cb: func(a serverAction) { Expect(a.data).To(Equal(nb)) }}, // Bad push results in a close serverAction{action: writeAction, data: errPayload.Bytes()}, serverAction{action: closeAction, data: []byte{}}, }, } It("should not return an error", func(d Done) { mockDone := make(chan interface{}) withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true go func() { n := <-c.FailedNotifs Expect(n.Notif.Identifier).To(Equal(uint32(9))) Expect(n.Notif.ID).To(Equal("some_rando")) close(mockDone) close(d) }() Expect(c.Send(n)).To(BeNil()) }) }) }) Context("closed, reconnect", func() { done := make(chan bool) n1 := apns.Notification{Identifier: 1} n1b, _ := n1.ToBinary() n1bcb := make([]byte, len(n1b)) errPayload := bytes.NewBuffer([]byte{}) binary.Write(errPayload, binary.BigEndian, uint8(8)) binary.Write(errPayload, binary.BigEndian, uint8(8)) binary.Write(errPayload, binary.BigEndian, uint32(2)) It("should not return an error", func(d Done) { mockDone := make(chan interface{}) as := [][]serverAction{ []serverAction{ // Write error serverAction{action: writeAction, data: errPayload.Bytes(), cb: func(a serverAction) { done <- true }}, // Close on error serverAction{action: closeAction, cb: func(a serverAction) { }}, }, []serverAction{ // Reconnect serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) { // Reconnected }}, // Read first good notification serverAction{action: readAction, data: n1bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n1b)) close(mockDone) close(d) }}, }, } withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true <-done time.Sleep(5 * time.Millisecond) // Good Expect(c.Send(n1)).To(BeNil()) }) }) }) Context("good, close, good, requeue of last good", func() { closed := make(chan bool) n1 := apns.Notification{Identifier: 1} n2 := apns.Notification{Identifier: 2} n1b, _ := n1.ToBinary() n2b, _ := n2.ToBinary() n1bcb := make([]byte, len(n1b)) n2bcb := make([]byte, len(n2b)) It("should not return an error", func(d Done) { mockDone := make(chan interface{}) as := [][]serverAction{ []serverAction{ // Connect serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) { // Handshake }}, // Read first good notification serverAction{action: readAction, data: n1bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n1b)) }}, // Close on error serverAction{action: closeAction, cb: func(a serverAction) { closed <- true }}, }, []serverAction{ // Reconnect serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) { // Reconnected }}, // Requeue serverAction{action: readAction, data: n2bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n2b)) close(mockDone) close(d) }}, }, } withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true // Good Expect(c.Send(n1)).To(BeNil()) <-closed time.Sleep(5 * time.Millisecond) // Good Expect(c.Send(n2)).To(BeNil()) }) }) }) Context("good, bad, good, requeue of last good", func() { It("should not return an error", func(d Done) { mockDone := make(chan interface{}) n1 := apns.Notification{Identifier: 1} n2 := apns.Notification{Identifier: 2} n3 := apns.Notification{Identifier: 3} n1b, _ := n1.ToBinary() n2b, _ := n2.ToBinary() n3b, _ := n3.ToBinary() n1bcb := make([]byte, len(n1b)) n2bcb := make([]byte, len(n2b)) n3bcb := make([]byte, len(n3b)) errPayload := bytes.NewBuffer([]byte{}) binary.Write(errPayload, binary.BigEndian, uint8(8)) binary.Write(errPayload, binary.BigEndian, uint8(8)) binary.Write(errPayload, binary.BigEndian, uint32(2)) as := [][]serverAction{ []serverAction{ // Connect serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) { // Handshake }}, // Read first good notification serverAction{action: readAction, data: n1bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n1b)) }}, // Read bad notification serverAction{action: readAction, data: n2bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n2b)) }}, // Read second good notification serverAction{action: readAction, data: n3bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n3b)) }}, // Write error serverAction{action: writeAction, data: errPayload.Bytes(), cb: func(a serverAction) { }}, // Close on error serverAction{action: closeAction, cb: func(a serverAction) { }}, }, []serverAction{ // Reconnect serverAction{action: readAction, data: []byte{}, cb: func(a serverAction) { // Reconnected }}, // Requeue serverAction{action: readAction, data: n3bcb, cb: func(a serverAction) { Expect(a.data).To(Equal(n3b)) close(mockDone) close(d) }}, }, } withMockServerAsync(as, mockDone, func(s *mockTLSServer) { c, _ := apns.NewClient(s.Address(), DummyCert, DummyKey) c.Conn.Conf.InsecureSkipVerify = true // Good Expect(c.Send(n1)).To(BeNil()) // Bad Expect(c.Send(n2)).To(BeNil()) // Good Expect(c.Send(n3)).To(BeNil()) }) }) }) }) }) ================================================ FILE: conn.go ================================================ package apns import ( "crypto/tls" "net" "strings" ) const ( ProductionGateway = "gateway.push.apple.com:2195" SandboxGateway = "gateway.sandbox.push.apple.com:2195" ProductionFeedbackGateway = "feedback.push.apple.com:2196" SandboxFeedbackGateway = "feedback.sandbox.push.apple.com:2196" ) // Conn is a wrapper for the actual TLS connections made to Apple type Conn struct { NetConn net.Conn Conf *tls.Config gateway string connected bool } func NewConnWithCert(gw string, cert tls.Certificate) Conn { gatewayParts := strings.Split(gw, ":") conf := tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: gatewayParts[0], } return Conn{gateway: gw, Conf: &conf} } // NewConnWithFiles creates a new Conn from certificate and key in the specified files func NewConn(gw string, crt string, key string) (Conn, error) { cert, err := tls.X509KeyPair([]byte(crt), []byte(key)) if err != nil { return Conn{}, err } return NewConnWithCert(gw, cert), nil } // NewConnWithFiles creates a new Conn from certificate and key in the specified files func NewConnWithFiles(gw string, certFile string, keyFile string) (Conn, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return Conn{}, err } return NewConnWithCert(gw, cert), nil } // Connect actually creates the TLS connection func (c *Conn) Connect() error { // Make sure the existing connection is closed if c.NetConn != nil { c.NetConn.Close() } conn, err := net.Dial("tcp", c.gateway) if err != nil { return err } tlsConn := tls.Client(conn, c.Conf) err = tlsConn.Handshake() if err != nil { return err } c.NetConn = tlsConn return nil } func (c *Conn) Close() error { if c.NetConn != nil { return c.NetConn.Close() } return nil } // Read reads data from the connection func (c *Conn) Read(p []byte) (int, error) { i, err := c.NetConn.Read(p) return i, err } // Write writes data from the connection func (c *Conn) Write(p []byte) (int, error) { return c.NetConn.Write(p) } ================================================ FILE: conn_test.go ================================================ package apns_test import ( "bytes" "crypto/tls" "fmt" "io" "io/ioutil" "log" "net" "os" "strings" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/timehop/apns" ) var DummyCert = `-----BEGIN CERTIFICATE----- MIIC9TCCAd+gAwIBAgIQf3bEgFWUb+q6eK5ySkV/gjALBgkqhkiG9w0BAQUwEjEQ MA4GA1UEChMHQWNtZSBDbzAeFw0xNDA2MzAwNDI5MDhaFw0xNTA2MzAwNDI5MDha MBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK AoIBAQDhAgWrrFZBtCfVEPg1tSIr9fuSUoeundb556IUr9uOmOHaYK7r3/I43acw bVIfaenFxwUUf8YakQzTjOa5qSfK/Eylyw2ezBJtNUEqcHw0f+y66+jJbZa4clPa tL6ezaMS/syXPpvNU8+16jdVdTJzqdBdSGAZMOCeumUWDNdlfBmHPVq1JMy0uGmO XDoZK2Ir0/3LUfjk9R2wdm1VLrJAml7F0L0FhBHHXgHOSFM2ixjGflffaiuTCxhW 1z1NTo9XjWUQh2iM9Udf+xVnJLGLZ0EMFr2qihuK604Fp4SlNHEF+UWUn+j0PYo+ LbzM9oKJcdVD0XI36vrn3rGPHO9vAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIAoDAT BgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxv Y2FsaG9zdDALBgkqhkiG9w0BAQUDggEBAGJ/3I4KKlbEwLAC5ut4ZZ9V8WF4sHkI Lj7e4vx2pPi6hf9miV1ff01NrpfUna7flwL9yD7Ybl7jRRIB4rIcKk+U5djGsT3H ScGkbIMKrr08drWw1g4JU6PBH7xTfzGxNRERrnmrbJV0jCo9Tt8i53IpPtp6Z2Q1 8ydtPhU+Bpe2YoNr1w1fSV1JHXqjKV8RlGkCNSi4ozPOO8RbAYnBT3d9XSGoX//q RGJUf3wC/rCxJkN63Moxuy3vxV2TmiqccHOrXJSJ8P/4PpPV/xuBk5k4HS1Nfmew d9WHHn6bMJE9arVvWAiu9teCadVffuS2cl2cicN4XB6Ui0aDqhG2Exw= -----END CERTIFICATE-----` var DummyKey = `-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA4QIFq6xWQbQn1RD4NbUiK/X7klKHrp3W+eeiFK/bjpjh2mCu 69/yON2nMG1SH2npxccFFH/GGpEM04zmuaknyvxMpcsNnswSbTVBKnB8NH/suuvo yW2WuHJT2rS+ns2jEv7Mlz6bzVPPteo3VXUyc6nQXUhgGTDgnrplFgzXZXwZhz1a tSTMtLhpjlw6GStiK9P9y1H45PUdsHZtVS6yQJpexdC9BYQRx14BzkhTNosYxn5X 32orkwsYVtc9TU6PV41lEIdojPVHX/sVZySxi2dBDBa9qoobiutOBaeEpTRxBflF lJ/o9D2KPi28zPaCiXHVQ9FyN+r6596xjxzvbwIDAQABAoIBAFzW+cIA5MJNdFX8 n32BlGzxHPEd7nAFHmuUwJKqkPwAZsg1NleK2qXOByr7IHRnvhZl7Nmtcu8JRHKR Y63ddtbRTUrnQmJwL3YyEAZTzVvYILRrnGxoNFU8jw7hnvllPdEbow0QvzZ0S3Lz BgvTxJJm0dt7fnNGcJftrsHvYHy1dptaR4hPv0xV5G7RPrbTl94llKfi745tp5Wd xGpnjcBXoAnzCVRij1tHfSYubRJ2MJV0kzG3oVdRV2P/zWaout8BlhLCURv4sRUX 7FfCNa/z+G6AlROjCKJUP9YIUbxBEa/aP8YlSiyLRi1jFbMWcnKWQUdqS19m73Ap a1LJFPECgYEA+Ve5DegcrWnUb2HsHD38HlmEg6S+/jg2P4TsuLZBtvO4/vzRx/qq pwuuMm2CsvXr4nVmMEsMlSzYdsnaXIlWqyVDCOwIWR5VYT2GDWqQLaIXPlFaISzN 27tHd64KUtR1fMJUwQVK/MUORUbpYoAnSIil2SlYkWUhF024fNP8CxcCgYEA5wP4 HLiqU2rqe7vSAF/8fHwPleTzuCfMCVZm0aegUzQQQtklZoVE/BBwEGHdXflq1veq pHeC8bNR4BF6ZgeSWgbLVF3msquy47QeNElHA2muJd3qmNWz4LXo1Pxb8KXcnXri QZ+r3Y8obWTFQYq7gGQGPLXGTV3bhLGIyrT4lWkCgYAgZ2MYSJL5gmhmNT6fCPsr 4oxTI2Ti2uFJ7fdppd3ybcgb8zU8HPpyjRUNXqf+o/EM1B78pbQz6skS3vau0fZe dZA5p5sKIeQMqBc0xSWJmKgWpDHnX9A8/yCxj/+tdgjytrqW/x4YrW9GV4nbEDaK uZ98EmB9PLxJMAOKzW3S7wKBgQDD4PCy4b3CR2iVC9dva/P5VXQdo+knX884p6M8 58YgZofXNqnouN2aYRG0QlbiBMcbiRqOo6tK58JnnEpNUuQ8I4Cqg4hGPSHMwv/N U8i70xLPltABUUpZIcVPOr92WBytBvHrtMiUb3tW7lf3T/vWTHmhZnvDQ+8LH0Ge pz4T6QKBgQCoBJKOd781IQmT6i5hHSYJlsP6ymaaaQniJPVpnci/jf8+2QtponQY scgnaBLBasLQ6GfKSRtcyidEi9wwxpVj0tw2p567jeNcIveD0TOYFf0RHEfrs+D4 VdRgai/v2NbFZLDnzeGVuYypXu6R78isJfHtz/a0aEave8yB3CRiDw== -----END RSA PRIVATE KEY-----` // To be able to run in parallel var mockPort = 50000 // Mock Addr type mockAddr struct { } func (m mockAddr) Network() string { return "localhost:56789" } func (m mockAddr) String() string { return "localhost:56789" } // Mock TLS connection type mockTLSNetConn struct { bb *bytes.Buffer err error } func (t mockTLSNetConn) Read(p []byte) (int, error) { r := bytes.NewReader(t.bb.Bytes()) return r.Read(p) } func (t mockTLSNetConn) Write(p []byte) (int, error) { return t.bb.Write(p) } func (t mockTLSNetConn) Close() error { return t.err } func (m mockTLSNetConn) LocalAddr() net.Addr { return mockAddr{} } func (m mockTLSNetConn) RemoteAddr() net.Addr { return mockAddr{} } func (m mockTLSNetConn) SetDeadline(t time.Time) error { return nil } func (m mockTLSNetConn) SetReadDeadline(t time.Time) error { return nil } func (m mockTLSNetConn) SetWriteDeadline(t time.Time) error { return nil } type serverAction struct { action string data []byte cb func(s serverAction) } const ( readAction = "read" writeAction = "write" closeAction = "close" ) type mockTLSServer struct { Port int Server net.Listener ConnectionActionGroups [][]serverAction } func (m *mockTLSServer) portStr() string { if m.Port == 0 { mockPort = mockPort + 1 m.Port = mockPort } return fmt.Sprint(m.Port) } func (m *mockTLSServer) Address() string { return "localhost:" + m.portStr() } func (m *mockTLSServer) start() { cert, err := tls.X509KeyPair([]byte(DummyCert), []byte(DummyKey)) if err != nil { log.Panic(err) } config := tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAnyClientCert} m.Server, err = tls.Listen("tcp", "localhost:"+m.portStr(), &config) go func() { for i := 0; i < len(m.ConnectionActionGroups); i++ { g := m.ConnectionActionGroups[i] // Wait for a connection. conn, err := m.Server.Accept() if err != nil { if strings.Contains(err.Error(), "use of closed network connection") { return } else { log.Fatal(err) } } // Handle the connection in a new goroutine. // The loop then returns to accepting, so that // multiple connections may be served concurrently. go func(c net.Conn) { for j := 0; j < len(g); j++ { a := g[j] switch a.action { case readAction: c.Read(a.data) case writeAction: c.Write(a.data) case closeAction: c.Close() if a.cb != nil { a.cb(a) } return } if a.cb != nil { a.cb(a) } } }(conn) } // No more connection action groups }() } func (m *mockTLSServer) stop() { if m.Server != nil { m.Server.Close() } } var withMockServer = func(as [][]serverAction, cb func(s *mockTLSServer)) { d := make(chan interface{}) withMockServerAsync(as, d, func(s *mockTLSServer) { cb(s) close(d) }) } var withMockServerAsync = func(as [][]serverAction, d chan interface{}, cb func(s *mockTLSServer)) { s := &mockTLSServer{} s.ConnectionActionGroups = as s.start() cb(s) <-d s.stop() } // Tests var _ = Describe("Conn", func() { Describe(".NewConn", func() { Context("bad key/cert pair", func() { It("should return an error", func() { _, err := apns.NewConn(apns.SandboxGateway, "missing", "missing") Expect(err).NotTo(BeNil()) }) }) Context("valid key/cert pair", func() { It("should not return an error", func() { _, err := apns.NewConn(apns.SandboxGateway, DummyCert, DummyKey) Expect(err).To(BeNil()) }) }) }) Describe(".NewConnWithFiles", func() { Context("missing files", func() { It("should return an error", func() { _, err := apns.NewConnWithFiles(apns.SandboxGateway, "missing.pem", "missing.pem") Expect(err).NotTo(BeNil()) }) }) Context("with valid cert/key pair", func() { var certFile, keyFile *os.File var err error BeforeEach(func() { certFile, _ = ioutil.TempFile("", "cert.pem") certFile.Write([]byte(DummyCert)) certFile.Close() keyFile, _ = ioutil.TempFile("", "key.pem") keyFile.Write([]byte(DummyKey)) keyFile.Close() }) AfterEach(func() { if certFile != nil { os.Remove(certFile.Name()) } if keyFile != nil { os.Remove(keyFile.Name()) } }) It("should returning a connection", func() { _, err = apns.NewConnWithFiles(apns.SandboxGateway, certFile.Name(), keyFile.Name()) Expect(err).To(BeNil()) }) }) }) Describe("#Connect()", func() { Context("server not up", func() { conn, _ := apns.NewConnWithFiles(apns.SandboxGateway, "missing.pem", "missing.pem") It("should return an error", func() { err := conn.Connect() Expect(err).NotTo(BeNil()) }) }) Context("server up", func() { as := [][]serverAction{[]serverAction{serverAction{action: readAction, data: []byte{}}}} Context("with untrusted certs", func() { It("should return an error", func(d Done) { withMockServer(as, func(s *mockTLSServer) { conn, _ := apns.NewConn(s.Address(), DummyCert, DummyKey) err := conn.Connect() Expect(err).NotTo(BeNil()) close(d) }) }) }) Context("trusting the certs", func() { It("should not return an error", func(d Done) { withMockServer(as, func(s *mockTLSServer) { conn, _ := apns.NewConn(s.Address(), DummyCert, DummyKey) conn.Conf.InsecureSkipVerify = true err := conn.Connect() Expect(err).To(BeNil()) close(d) }) }) }) Context("with existing connection", func() { It("should not return an error", func(d Done) { as = [][]serverAction{ []serverAction{serverAction{action: readAction, data: []byte{}}}, []serverAction{serverAction{action: readAction, data: []byte{}}}, } withMockServer(as, func(s *mockTLSServer) { conn, _ := apns.NewConn(s.Address(), DummyCert, DummyKey) conn.Conf.InsecureSkipVerify = true conn.Connect() err := conn.Connect() Expect(err).To(BeNil()) close(d) }) }) }) }) }) Describe("#Read", func() { rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte("hello!"))} pp := make([]byte, 6) bytes.NewReader(rwc.bb.Bytes()).Read(pp) conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey) conn.NetConn = rwc It("should read out 'hello!'", func() { p := make([]byte, 6) conn.Read(p) Expect(p).To(Equal([]byte("hello!"))) }) }) Describe("#Write", func() { rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte{})} conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey) conn.NetConn = rwc It("should write out 'world!'", func() { conn.Write([]byte("world!")) Expect(rwc.bb.String()).To(Equal("world!")) }) }) Describe("#Close", func() { Context("with connection", func() { Context("no error", func() { rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte{})} conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey) conn.NetConn = rwc It("should return no error", func() { Expect(rwc.Close()).To(BeNil()) }) }) Context("with error", func() { rwc := mockTLSNetConn{bb: bytes.NewBuffer([]byte{})} conn, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey) conn.NetConn = rwc rwc.err = io.EOF It("should return that error", func() { Expect(rwc.Close()).To(Equal(io.EOF)) }) }) }) Context("without connection", func() { c, _ := apns.NewConn(apns.ProductionGateway, DummyCert, DummyKey) It("should not return an error", func() { Expect(c.Close()).To(BeNil()) }) }) }) }) ================================================ FILE: doc.go ================================================ /* A Go package to interface with the Apple Push Notification Service Features This library implements a few features that we couldn't find in any one library elsewhere: Long Lived Clients - Apple's documentation say that you should hold a persistent connection open and not create new connections for every payload See: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW6) Use of New Protocol - Apple came out with v2 of their API with support for variable length payloads. This library uses that protocol. Robust Send Guarantees - APNS has asynchronous feedback on whether a push sent. That means that if you send pushes after a bad send, those pushes will be lost forever. Our library records the last N pushes, detects errors, and is able to resend the pushes that could have been lost. See: http://redth.codes/the-problem-with-apples-push-notification-ser/ */ package apns ================================================ FILE: error.go ================================================ package apns import ( "bytes" "encoding/binary" ) const ( // Error strings based on the codes specified here: // https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW12 ErrProcessing = "Processing error" ErrMissingDeviceToken = "Missing device token" ErrMissingTopic = "Missing topic" ErrMissingPayload = "Missing payload" ErrInvalidTokenSize = "Invalid token size" ErrInvalidTopicSize = "Invalid topic size" ErrInvalidPayloadSize = "Invalid payload size" ErrInvalidToken = "Invalid token" ErrShutdown = "Shutdown" ErrUnknown = "None (unknown)" ) var errorMapping = map[uint8]string{ 1: ErrProcessing, 2: ErrMissingDeviceToken, 3: ErrMissingTopic, 4: ErrMissingPayload, 5: ErrInvalidTokenSize, 6: ErrInvalidTopicSize, 7: ErrInvalidPayloadSize, 8: ErrInvalidToken, 10: ErrShutdown, 255: ErrUnknown, } type Error struct { Command uint8 Status uint8 Identifier uint32 ErrStr string } func NewError(p []byte) Error { if len(p) != 1+1+4 { return Error{ErrStr: ErrUnknown} } r := bytes.NewBuffer(p) e := Error{} binary.Read(r, binary.BigEndian, &e.Command) binary.Read(r, binary.BigEndian, &e.Status) binary.Read(r, binary.BigEndian, &e.Identifier) var ok bool if e.ErrStr, ok = errorMapping[e.Status]; !ok { e.ErrStr = ErrUnknown } return e } func (e *Error) Error() string { return e.ErrStr } ================================================ FILE: error_test.go ================================================ package apns_test import ( "bytes" "encoding/binary" "math/rand" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/timehop/apns" ) var _ = Describe("Error", func() { Describe(".NewError", func() { ShouldBeErrorWithErrStr := func(status int, errStr string) { var errPayload = func(command int, status int, identifier int) []byte { buffer := bytes.NewBuffer([]byte{}) binary.Write(buffer, binary.BigEndian, uint8(command)) binary.Write(buffer, binary.BigEndian, uint8(status)) binary.Write(buffer, binary.BigEndian, uint32(identifier)) return buffer.Bytes() } command := rand.Int() identifier := rand.Int() p := errPayload(command, status, identifier) e := apns.NewError(p) It("should parse the field out correctly", func() { Expect(e.Command).To(Equal(uint8(command))) Expect(e.Status).To(Equal(uint8(status))) Expect(e.Identifier).To(Equal(uint32(identifier))) }) It("should have picked the right error string", func() { Expect(e.ErrStr).To(Equal(errStr)) }) } Context("processing error", func() { ShouldBeErrorWithErrStr(1, apns.ErrProcessing) }) Context("device token error", func() { ShouldBeErrorWithErrStr(2, apns.ErrMissingDeviceToken) }) Context("missing topic error", func() { ShouldBeErrorWithErrStr(3, apns.ErrMissingTopic) }) Context("missing payload error", func() { ShouldBeErrorWithErrStr(4, apns.ErrMissingPayload) }) Context("invalid token size error", func() { ShouldBeErrorWithErrStr(5, apns.ErrInvalidTokenSize) }) Context("invalid topic size error", func() { ShouldBeErrorWithErrStr(6, apns.ErrInvalidTopicSize) }) Context("invalid payload size error", func() { ShouldBeErrorWithErrStr(7, apns.ErrInvalidPayloadSize) }) Context("invalid token error", func() { ShouldBeErrorWithErrStr(8, apns.ErrInvalidToken) }) Context("shutdown error", func() { ShouldBeErrorWithErrStr(10, apns.ErrShutdown) }) Context("unknown error", func() { ShouldBeErrorWithErrStr(255, apns.ErrUnknown) }) Context("error with unrecognized code", func() { ShouldBeErrorWithErrStr(300, apns.ErrUnknown) }) Context("not enough bytes", func() { It("should be ErrUnknown", func() { e := apns.NewError([]byte{}) Expect(e).NotTo(BeNil()) Expect(e.ErrStr).To(Equal(apns.ErrUnknown)) }) }) }) Describe("#Error", func() { It("should have an error string", func() { e := apns.Error{ErrStr: "this is an error string"} Expect(e.Error()).To(Equal("this is an error string")) }) }) }) ================================================ FILE: example/example.go ================================================ package main import ( "fmt" "log" "github.com/timehop/apns" ) func main() { c, err := apns.NewClientWithFiles(apns.ProductionGateway, "apns.crt", "apns.key") if err != nil { log.Fatal("Could not create client", err.Error()) } i := 0 for { fmt.Print("Enter ' ': ") var tok, body string var badge uint _, err := fmt.Scanf("%s %d %s", &tok, &badge, &body) if err != nil { fmt.Printf("Something went wrong: %v\n", err.Error()) continue } p := apns.NewPayload() p.APS.Alert.Body = body p.APS.Badge.Set(badge) p.SetCustomValue("link", "yourapp://precache/20140718") m := apns.NewNotification() m.Payload = p m.DeviceToken = tok m.Priority = apns.PriorityImmediate m.Identifier = uint32(i) c.Send(m) i++ } } ================================================ FILE: feedback.go ================================================ package apns import ( "bytes" "crypto/tls" "encoding/binary" "encoding/hex" "time" ) type Feedback struct { Conn *Conn } type FeedbackTuple struct { Timestamp time.Time TokenLength uint16 DeviceToken string } func feedbackTupleFromBytes(b []byte) FeedbackTuple { r := bytes.NewReader(b) var ts uint32 binary.Read(r, binary.BigEndian, &ts) var tokLen uint16 binary.Read(r, binary.BigEndian, &tokLen) tok := make([]byte, tokLen) binary.Read(r, binary.BigEndian, &tok) return FeedbackTuple{ Timestamp: time.Unix(int64(ts), 0), TokenLength: tokLen, DeviceToken: hex.EncodeToString(tok), } } func NewFeedbackWithCert(gw string, cert tls.Certificate) Feedback { conn := NewConnWithCert(gw, cert) return Feedback{Conn: &conn} } func NewFeedback(gw string, cert string, key string) (Feedback, error) { conn, err := NewConn(gw, cert, key) if err != nil { return Feedback{}, err } return Feedback{Conn: &conn}, nil } func NewFeedbackWithFiles(gw string, certFile string, keyFile string) (Feedback, error) { conn, err := NewConnWithFiles(gw, certFile, keyFile) if err != nil { return Feedback{}, err } return Feedback{Conn: &conn}, nil } // Receive returns a read only channel for APNs feedback. The returned channel // will close when there is no more data to be read. func (f Feedback) Receive() <-chan FeedbackTuple { fc := make(chan FeedbackTuple) go f.receive(fc) return fc } func (f Feedback) receive(fc chan FeedbackTuple) { err := f.Conn.Connect() if err != nil { close(fc) return } defer f.Conn.Close() for { b := make([]byte, 38) f.Conn.NetConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) _, err := f.Conn.Read(b) if err != nil { close(fc) return } fc <- feedbackTupleFromBytes(b) } } ================================================ FILE: feedback_test.go ================================================ package apns_test import ( "bytes" "encoding/binary" "encoding/hex" "io/ioutil" "os" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/timehop/apns" ) var _ = Describe("Feedback", func() { Describe(".NewFeedback", func() { Context("bad cert/key pair", func() { It("should error out", func() { _, err := apns.NewFeedback(apns.ProductionGateway, "missing", "missing_also") Expect(err).NotTo(BeNil()) }) }) Context("valid cert/key pair", func() { It("should create a valid client", func() { _, err := apns.NewFeedback(apns.ProductionGateway, DummyCert, DummyKey) Expect(err).To(BeNil()) }) }) }) Describe(".NewFeedbackWithFiles", func() { Context("missing cert/key pair", func() { It("should error out", func() { _, err := apns.NewFeedbackWithFiles(apns.ProductionGateway, "missing", "missing_also") Expect(err).NotTo(BeNil()) }) }) Context("valid cert/key pair", func() { var certFile, keyFile *os.File BeforeEach(func() { certFile, _ = ioutil.TempFile("", "cert.pem") certFile.Write([]byte(DummyCert)) certFile.Close() keyFile, _ = ioutil.TempFile("", "key.pem") keyFile.Write([]byte(DummyKey)) keyFile.Close() }) AfterEach(func() { if certFile != nil { os.Remove(certFile.Name()) } if keyFile != nil { os.Remove(keyFile.Name()) } }) It("should create a valid client", func() { _, err := apns.NewFeedbackWithFiles(apns.ProductionGateway, certFile.Name(), keyFile.Name()) Expect(err).To(BeNil()) }) }) }) Describe("#Receive", func() { Context("could not connect", func() { It("should not receive anything", func() { s := &mockTLSServer{} f, _ := apns.NewFeedback(s.Address(), DummyCert, DummyKey) f.Conn.Conf.InsecureSkipVerify = true c := f.Receive() r := 0 for _ = range c { r += 1 } Expect(r).To(Equal(0)) }) }) Context("times out", func() { as := [][]serverAction{ []serverAction{ serverAction{action: readAction, data: []byte{}}, }, } withMockServer(as, func(s *mockTLSServer) { f, _ := apns.NewFeedback(s.Address(), DummyCert, DummyKey) f.Conn.Conf.InsecureSkipVerify = true It("should not receive anything", func() { c := f.Receive() r := 0 for _ = range c { r += 1 } Expect(r).To(Equal(0)) }) }) }) Context("with feedback", func() { f1 := bytes.NewBuffer([]byte{}) f2 := bytes.NewBuffer([]byte{}) f3 := bytes.NewBuffer([]byte{}) // The final token strings t1 := "00a18269661e9406aea59a5620b05c7c0e371574fa6f251951de8d7a5a292535" t2 := "00a1a4b7294fcfbc5293f63d4298fcecd9c20a893befd45adceead5fc92d3319" t3 := "00a1b7893d5e85eb8bb7bf0846b464d075248555118ae893b06e96cfb8d678e3" bt1, _ := hex.DecodeString(t1) bt2, _ := hex.DecodeString(t2) bt3, _ := hex.DecodeString(t3) binary.Write(f1, binary.BigEndian, uint32(1404358249)) binary.Write(f1, binary.BigEndian, uint16(len(bt1))) binary.Write(f1, binary.BigEndian, bt1) binary.Write(f2, binary.BigEndian, uint32(1404352249)) binary.Write(f2, binary.BigEndian, uint16(len(bt2))) binary.Write(f2, binary.BigEndian, bt2) binary.Write(f3, binary.BigEndian, uint32(1394352249)) binary.Write(f3, binary.BigEndian, uint16(len(bt3))) binary.Write(f3, binary.BigEndian, bt3) as := [][]serverAction{ []serverAction{ serverAction{action: writeAction, data: f1.Bytes()}, serverAction{action: writeAction, data: f2.Bytes()}, serverAction{action: writeAction, data: f3.Bytes()}, }, } It("should receive feedback", func(d Done) { withMockServer(as, func(s *mockTLSServer) { f, _ := apns.NewFeedback(s.Address(), DummyCert, DummyKey) f.Conn.Conf.InsecureSkipVerify = true c := f.Receive() r1 := <-c Expect(r1.Timestamp).To(Equal(time.Unix(1404358249, 0))) Expect(r1.TokenLength).To(Equal(uint16(len(bt1)))) Expect(r1.DeviceToken).To(Equal(t1)) r2 := <-c Expect(r2.Timestamp).To(Equal(time.Unix(1404352249, 0))) Expect(r2.TokenLength).To(Equal(uint16(len(bt2)))) Expect(r2.DeviceToken).To(Equal(t2)) r3 := <-c Expect(r3.Timestamp).To(Equal(time.Unix(1394352249, 0))) Expect(r3.TokenLength).To(Equal(uint16(len(bt3)))) Expect(r3.DeviceToken).To(Equal(t3)) <-c close(d) }) }) }) }) }) ================================================ FILE: go.mod ================================================ module github.com/timehop/apns go 1.20 require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.27.6 ) require ( github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/nxadm/tail v1.4.8 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: notification.go ================================================ package apns import ( "bytes" "encoding/binary" "encoding/hex" "encoding/json" "errors" "fmt" "time" ) const ( PriorityImmediate = 10 PriorityPowerConserve = 5 ) const ( commandID = 2 // Items IDs deviceTokenItemID = 1 payloadItemID = 2 notificationIdentifierItemID = 3 expirationDateItemID = 4 priorityItemID = 5 // Item lengths deviceTokenItemLength = 32 notificationIdentifierItemLength = 4 expirationDateItemLength = 4 priorityItemLength = 1 ) type NotificationResult struct { Notif Notification Err Error } type Alert struct { // Do not add fields without updating the implementation of isZero. Body string `json:"body,omitempty"` Title string `json:"title,omitempty"` Action string `json:"action,omitempty"` LocKey string `json:"loc-key,omitempty"` LocArgs []string `json:"loc-args,omitempty"` ActionLocKey string `json:"action-loc-key,omitempty"` LaunchImage string `json:"launch-image,omitempty"` } func (a *Alert) isSimple() bool { return len(a.Title) == 0 && len(a.Action) == 0 && len(a.LocKey) == 0 && len(a.LocArgs) == 0 && len(a.ActionLocKey) == 0 && len(a.LaunchImage) == 0 } func (a *Alert) isZero() bool { return a.isSimple() && len(a.Body) == 0 } type APS struct { Alert Alert Badge BadgeNumber Sound string ContentAvailable int URLArgs []string Category string // requires iOS 8+ AccountId string // for email push notifications } func (aps APS) MarshalJSON() ([]byte, error) { data := make(map[string]interface{}) if !aps.Alert.isZero() { if aps.Alert.isSimple() { data["alert"] = aps.Alert.Body } else { data["alert"] = aps.Alert } } if aps.Badge.IsSet { data["badge"] = aps.Badge } if aps.Sound != "" { data["sound"] = aps.Sound } if aps.ContentAvailable != 0 { data["content-available"] = aps.ContentAvailable } if aps.Category != "" { data["category"] = aps.Category } if aps.URLArgs != nil && len(aps.URLArgs) != 0 { data["url-args"] = aps.URLArgs } if aps.AccountId != "" { data["account-id"] = aps.AccountId } return json.Marshal(data) } type Payload struct { APS APS // MDM for mobile device management MDM string customValues map[string]interface{} } func (p *Payload) MarshalJSON() ([]byte, error) { if len(p.MDM) != 0 { p.customValues["mdm"] = p.MDM } else { p.customValues["aps"] = p.APS } return json.Marshal(p.customValues) } func (p *Payload) SetCustomValue(key string, value interface{}) error { if key == "aps" { return errors.New("cannot assign a custom APS value in payload") } p.customValues[key] = value return nil } type Notification struct { ID string DeviceToken string Identifier uint32 Expiration *time.Time Priority int Payload *Payload } func NewNotification() Notification { return Notification{Payload: NewPayload()} } func NewPayload() *Payload { return &Payload{customValues: map[string]interface{}{}} } func (n Notification) ToBinary() ([]byte, error) { b := []byte{} binTok, err := hex.DecodeString(n.DeviceToken) if err != nil { return b, fmt.Errorf("convert token to hex error: %s", err) } j, _ := json.Marshal(n.Payload) buf := bytes.NewBuffer(b) // Token binary.Write(buf, binary.BigEndian, uint8(deviceTokenItemID)) binary.Write(buf, binary.BigEndian, uint16(deviceTokenItemLength)) binary.Write(buf, binary.BigEndian, binTok) // Payload binary.Write(buf, binary.BigEndian, uint8(payloadItemID)) binary.Write(buf, binary.BigEndian, uint16(len(j))) binary.Write(buf, binary.BigEndian, j) // Identifier binary.Write(buf, binary.BigEndian, uint8(notificationIdentifierItemID)) binary.Write(buf, binary.BigEndian, uint16(notificationIdentifierItemLength)) binary.Write(buf, binary.BigEndian, uint32(n.Identifier)) // Expiry binary.Write(buf, binary.BigEndian, uint8(expirationDateItemID)) binary.Write(buf, binary.BigEndian, uint16(expirationDateItemLength)) if n.Expiration == nil { binary.Write(buf, binary.BigEndian, uint32(0)) } else { binary.Write(buf, binary.BigEndian, uint32(n.Expiration.Unix())) } // Priority binary.Write(buf, binary.BigEndian, uint8(priorityItemID)) binary.Write(buf, binary.BigEndian, uint16(priorityItemLength)) binary.Write(buf, binary.BigEndian, uint8(n.Priority)) framebuf := bytes.NewBuffer([]byte{}) binary.Write(framebuf, binary.BigEndian, uint8(commandID)) binary.Write(framebuf, binary.BigEndian, uint32(buf.Len())) binary.Write(framebuf, binary.BigEndian, buf.Bytes()) return framebuf.Bytes(), nil } ================================================ FILE: notification_test.go ================================================ package apns_test import ( "bytes" "encoding/binary" "encoding/json" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/timehop/apns" ) var _ = Describe("Notifications", func() { Describe("Alert", func() { Describe("JSON marshalling", func() { Context("only body", func() { It("should just have that field", func() { a := apns.Alert{Body: "whatup"} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"body":"whatup"}`))) }) }) Context("only loc-key", func() { It("should just have that field", func() { a := apns.Alert{LocKey: "localization"} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"loc-key":"localization"}`))) }) }) Context("only loc-args", func() { It("should just have that field", func() { a := apns.Alert{LocArgs: []string{"world", "cup"}} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"loc-args":["world","cup"]}`))) }) }) Context("only action-loc-key", func() { It("should just have that field", func() { a := apns.Alert{ActionLocKey: "akshun localization"} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"action-loc-key":"akshun localization"}`))) }) }) Context("only launch image", func() { It("should just have that field", func() { a := apns.Alert{LaunchImage: "dee fault"} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"launch-image":"dee fault"}`))) }) }) Context("fully loaded", func() { It("should serialize", func() { a := apns.Alert{Body: "USA scores!", LocKey: "game", LocArgs: []string{"USA", "BRA"}, LaunchImage: "scoreboard"} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"body":"USA scores!","loc-key":"game","loc-args":["USA","BRA"],"launch-image":"scoreboard"}`))) }) }) }) }) Describe("Safari", func() { Describe("#MarshalJSON", func() { Context("with complete payload", func() { It("should marshal APS", func() { p := apns.NewPayload() p.APS.Alert.Title = "Hello World!" p.APS.Alert.Body = "This is a body" p.APS.Alert.Action = "Launch" p.APS.URLArgs = []string{"hello", "world"} b, err := json.Marshal(p) Expect(err).To(BeNil()) Expect(b).To(Equal([]byte(`{"aps":{"alert":{"body":"This is a body","title":"Hello World!","action":"Launch"},"url-args":["hello","world"]}}`))) }) }) }) }) Describe("Payload", func() { Describe("#MarshalJSON", func() { Context("no alert (as with Passbook)", func() { It("should not contain the alert struct", func() { p := apns.NewPayload() b, err := json.Marshal(p) Expect(err).To(BeNil()) Expect(b).To(Equal([]byte(`{"aps":{}}`))) }) }) Context("no alert with content available (as with Newsstand)", func() { It("should not contain the alert struct", func() { p := apns.NewPayload() p.APS.ContentAvailable = 1 b, err := json.Marshal(p) Expect(err).To(BeNil()) Expect(b).To(Equal([]byte(`{"aps":{"content-available":1}}`))) }) }) Context("with only APS", func() { It("should marshal APS", func() { p := apns.NewPayload() p.APS.Alert.Body = "testing" b, err := json.Marshal(p) Expect(err).To(BeNil()) Expect(b).To(Equal([]byte(`{"aps":{"alert":"testing"}}`))) }) }) Context("with custom attributes APS", func() { It("should marshal APS", func() { p := apns.NewPayload() p.APS.Alert.Body = "testing" p.SetCustomValue("email", "come@me.bro") b, err := json.Marshal(p) Expect(err).To(BeNil()) Expect(b).To(Equal([]byte(`{"aps":{"alert":"testing"},"email":"come@me.bro"}`))) }) }) Context("with only MDM", func() { It("should marshal MDM", func() { p := apns.NewPayload() p.MDM = "00000000-1111-3333-4444-555555555555" b, err := json.Marshal(p) Expect(err).To(BeNil()) Expect(b).To(Equal([]byte(`{"mdm":"00000000-1111-3333-4444-555555555555"}`))) }) }) }) }) Describe("APS", func() { Context("badge with a zero (clears notifications)", func() { It("should contain zero", func() { a := apns.APS{} a.Badge.Set(0) j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"badge":0}`))) }) }) Context("no badge specified (do nothing)", func() { It("should omit the badge field", func() { a := apns.APS{} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{}`))) }) }) Context("email account id", func() { It("should contain the account id", func() { a := apns.APS{AccountId: "1234"} j, err := json.Marshal(a) Expect(err).To(BeNil()) Expect(j).To(Equal([]byte(`{"account-id":"1234"}`))) }) }) }) Describe("Notification", func() { Describe("#ToBinary", func() { Context("invalid token format", func() { n := apns.NewNotification() n.DeviceToken = "totally not a valid token" It("should return an error", func() { _, err := n.ToBinary() Expect(err).NotTo(BeNil()) Expect(err.Error()).To(ContainSubstring("convert token to hex error")) }) }) Context("valid payload", func() { It("should generate the correct byte payload with expiry", func() { t := time.Unix(1404102833, 0) n := apns.NewNotification() n.Identifier = uint32(123123) n.DeviceToken = "9999999999999999999999999999999999999999999999999999999999999999" n.Priority = apns.PriorityImmediate n.Expiration = &t b, err := n.ToBinary() Expect(err).To(BeNil()) buf := bytes.NewBuffer(b) var expiry uint32 buf.Next(1 + 4 + 1 + 2 + 32 + 1 + 2 + 20 + 1 + 2 + 4 + 1 + 2) // Expiry binary.Read(buf, binary.BigEndian, &expiry) }) It("should generate the correct byte payload", func() { n := apns.NewNotification() n.Identifier = uint32(123123) n.DeviceToken = "9999999999999999999999999999999999999999999999999999999999999999" n.Priority = apns.PriorityImmediate b, err := n.ToBinary() Expect(err).To(BeNil()) buf := bytes.NewBuffer(b) var command, tokID, payloadID, identifierID, expiryID, priorityID uint8 var tokLen, payloadLen, identifierLen, expiryLen, priorityLen uint16 var frameLen, identifier, expiry uint32 var priority byte var tok [32]byte var payload [10]byte binary.Read(buf, binary.BigEndian, &command) binary.Read(buf, binary.BigEndian, &frameLen) // Token binary.Read(buf, binary.BigEndian, &tokID) binary.Read(buf, binary.BigEndian, &tokLen) binary.Read(buf, binary.BigEndian, &tok) // Payload binary.Read(buf, binary.BigEndian, &payloadID) binary.Read(buf, binary.BigEndian, &payloadLen) binary.Read(buf, binary.BigEndian, &payload) // Identifier binary.Read(buf, binary.BigEndian, &identifierID) binary.Read(buf, binary.BigEndian, &identifierLen) binary.Read(buf, binary.BigEndian, &identifier) // Expiry binary.Read(buf, binary.BigEndian, &expiryID) binary.Read(buf, binary.BigEndian, &expiryLen) binary.Read(buf, binary.BigEndian, &expiry) // Priority binary.Read(buf, binary.BigEndian, &priorityID) binary.Read(buf, binary.BigEndian, &priorityLen) binary.Read(buf, binary.BigEndian, &priority) Expect(command).To(Equal(uint8(2))) Expect(frameLen).To(Equal(uint32(66))) // Token Expect(tokID).To(Equal(uint8(1))) Expect(tokLen).To(Equal(uint16(32))) Expect(tok).To(Equal([32]byte{153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153})) // Payload Expect(payloadID).To(Equal(uint8(2))) Expect(payloadLen).To(Equal(uint16(10))) Expect(payload).To(Equal([10]byte{123, 34, 97, 112, 115, 34, 58, 123, 125, 125})) // Identifier Expect(identifierID).To(Equal(uint8(3))) Expect(identifierLen).To(Equal(uint16(4))) Expect(identifier).To(Equal(uint32(123123))) // Expiry Expect(expiryID).To(Equal(uint8(4))) Expect(expiryLen).To(Equal(uint16(4))) Expect(expiry).To(Equal(uint32(0))) // Priority Expect(priorityID).To(Equal(uint8(5))) Expect(priorityLen).To(Equal(uint16(1))) Expect(priority).To(Equal(uint8(10))) }) }) }) }) })