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
[](https://godoc.org/github.com/timehop/apns)
[](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 '<token> <badge> <msg>': ")
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)))
})
})
})
})
})
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
SYMBOL INDEX (100 symbols across 9 files)
FILE: apns_suite_test.go
function TestApns (line 10) | func TestApns(t *testing.T) {
FILE: badge_number.go
type BadgeNumber (line 10) | type BadgeNumber struct
method Unset (line 17) | func (b *BadgeNumber) Unset() {
method Set (line 25) | func (b *BadgeNumber) Set(number uint) {
method MarshalJSON (line 32) | func (b BadgeNumber) MarshalJSON() ([]byte, error) {
method UnmarshalJSON (line 40) | func (b *BadgeNumber) UnmarshalJSON(data []byte) error {
FILE: client.go
type buffer (line 11) | type buffer struct
method Add (line 20) | func (b *buffer) Add(v interface{}) *list.Element {
function newBuffer (line 16) | func newBuffer(size int) *buffer {
type Client (line 30) | type Client struct
method Send (line 75) | func (c *Client) Send(n Notification) error {
method reportFailedPush (line 80) | func (c *Client) reportFailedPush(v interface{}, err *Error) {
method requeue (line 92) | func (c *Client) requeue(cursor *list.Element) {
method handleError (line 102) | func (c *Client) handleError(err *Error, buffer *buffer) *list.Element {
method runLoop (line 125) | func (c *Client) runLoop() {
function newClientWithConn (line 38) | func newClientWithConn(gw string, conn Conn) Client {
function NewClientWithCert (line 51) | func NewClientWithCert(gw string, cert tls.Certificate) Client {
function NewClient (line 57) | func NewClient(gw string, cert string, key string) (Client, error) {
function NewClientWithFiles (line 66) | func NewClientWithFiles(gw string, certFile string, keyFile string) (Cli...
function readErrs (line 203) | func readErrs(c *Conn) chan error {
FILE: conn.go
constant ProductionGateway (line 10) | ProductionGateway = "gateway.push.apple.com:2195"
constant SandboxGateway (line 11) | SandboxGateway = "gateway.sandbox.push.apple.com:2195"
constant ProductionFeedbackGateway (line 13) | ProductionFeedbackGateway = "feedback.push.apple.com:2196"
constant SandboxFeedbackGateway (line 14) | SandboxFeedbackGateway = "feedback.sandbox.push.apple.com:2196"
type Conn (line 18) | type Conn struct
method Connect (line 57) | func (c *Conn) Connect() error {
method Close (line 78) | func (c *Conn) Close() error {
method Read (line 87) | func (c *Conn) Read(p []byte) (int, error) {
method Write (line 93) | func (c *Conn) Write(p []byte) (int, error) {
function NewConnWithCert (line 26) | func NewConnWithCert(gw string, cert tls.Certificate) Conn {
function NewConn (line 37) | func NewConn(gw string, crt string, key string) (Conn, error) {
function NewConnWithFiles (line 47) | func NewConnWithFiles(gw string, certFile string, keyFile string) (Conn,...
FILE: conn_test.go
type mockAddr (line 70) | type mockAddr struct
method Network (line 73) | func (m mockAddr) Network() string {
method String (line 77) | func (m mockAddr) String() string {
type mockTLSNetConn (line 82) | type mockTLSNetConn struct
method Read (line 87) | func (t mockTLSNetConn) Read(p []byte) (int, error) {
method Write (line 92) | func (t mockTLSNetConn) Write(p []byte) (int, error) {
method Close (line 96) | func (t mockTLSNetConn) Close() error {
method LocalAddr (line 100) | func (m mockTLSNetConn) LocalAddr() net.Addr {
method RemoteAddr (line 104) | func (m mockTLSNetConn) RemoteAddr() net.Addr {
method SetDeadline (line 108) | func (m mockTLSNetConn) SetDeadline(t time.Time) error {
method SetReadDeadline (line 112) | func (m mockTLSNetConn) SetReadDeadline(t time.Time) error {
method SetWriteDeadline (line 116) | func (m mockTLSNetConn) SetWriteDeadline(t time.Time) error {
type serverAction (line 120) | type serverAction struct
constant readAction (line 127) | readAction = "read"
constant writeAction (line 128) | writeAction = "write"
constant closeAction (line 129) | closeAction = "close"
type mockTLSServer (line 132) | type mockTLSServer struct
method portStr (line 138) | func (m *mockTLSServer) portStr() string {
method Address (line 147) | func (m *mockTLSServer) Address() string {
method start (line 151) | func (m *mockTLSServer) start() {
method stop (line 204) | func (m *mockTLSServer) stop() {
FILE: error.go
constant ErrProcessing (line 11) | ErrProcessing = "Processing error"
constant ErrMissingDeviceToken (line 12) | ErrMissingDeviceToken = "Missing device token"
constant ErrMissingTopic (line 13) | ErrMissingTopic = "Missing topic"
constant ErrMissingPayload (line 14) | ErrMissingPayload = "Missing payload"
constant ErrInvalidTokenSize (line 15) | ErrInvalidTokenSize = "Invalid token size"
constant ErrInvalidTopicSize (line 16) | ErrInvalidTopicSize = "Invalid topic size"
constant ErrInvalidPayloadSize (line 17) | ErrInvalidPayloadSize = "Invalid payload size"
constant ErrInvalidToken (line 18) | ErrInvalidToken = "Invalid token"
constant ErrShutdown (line 19) | ErrShutdown = "Shutdown"
constant ErrUnknown (line 20) | ErrUnknown = "None (unknown)"
type Error (line 36) | type Error struct
method Error (line 63) | func (e *Error) Error() string {
function NewError (line 43) | func NewError(p []byte) Error {
FILE: example/example.go
function main (line 10) | func main() {
FILE: feedback.go
type Feedback (line 11) | type Feedback struct
method Receive (line 66) | func (f Feedback) Receive() <-chan FeedbackTuple {
method receive (line 72) | func (f Feedback) receive(fc chan FeedbackTuple) {
type FeedbackTuple (line 15) | type FeedbackTuple struct
function feedbackTupleFromBytes (line 21) | func feedbackTupleFromBytes(b []byte) FeedbackTuple {
function NewFeedbackWithCert (line 40) | func NewFeedbackWithCert(gw string, cert tls.Certificate) Feedback {
function NewFeedback (line 46) | func NewFeedback(gw string, cert string, key string) (Feedback, error) {
function NewFeedbackWithFiles (line 55) | func NewFeedbackWithFiles(gw string, certFile string, keyFile string) (F...
FILE: notification.go
constant PriorityImmediate (line 14) | PriorityImmediate = 10
constant PriorityPowerConserve (line 15) | PriorityPowerConserve = 5
constant commandID (line 19) | commandID = 2
constant deviceTokenItemID (line 22) | deviceTokenItemID = 1
constant payloadItemID (line 23) | payloadItemID = 2
constant notificationIdentifierItemID (line 24) | notificationIdentifierItemID = 3
constant expirationDateItemID (line 25) | expirationDateItemID = 4
constant priorityItemID (line 26) | priorityItemID = 5
constant deviceTokenItemLength (line 29) | deviceTokenItemLength = 32
constant notificationIdentifierItemLength (line 30) | notificationIdentifierItemLength = 4
constant expirationDateItemLength (line 31) | expirationDateItemLength = 4
constant priorityItemLength (line 32) | priorityItemLength = 1
type NotificationResult (line 35) | type NotificationResult struct
type Alert (line 40) | type Alert struct
method isSimple (line 51) | func (a *Alert) isSimple() bool {
method isZero (line 55) | func (a *Alert) isZero() bool {
type APS (line 59) | type APS struct
method MarshalJSON (line 69) | func (aps APS) MarshalJSON() ([]byte, error) {
type Payload (line 101) | type Payload struct
method MarshalJSON (line 108) | func (p *Payload) MarshalJSON() ([]byte, error) {
method SetCustomValue (line 118) | func (p *Payload) SetCustomValue(key string, value interface{}) error {
type Notification (line 128) | type Notification struct
method ToBinary (line 145) | func (n Notification) ToBinary() ([]byte, error) {
function NewNotification (line 137) | func NewNotification() Notification {
function NewPayload (line 141) | func NewPayload() *Payload {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (80K chars).
[
{
"path": ".gitignore",
"chars": 348,
"preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture spe"
},
{
"path": ".travis.yml",
"chars": 336,
"preview": "language: go\ngo:\n - 1.3\nservices:\n - redis-server\nbefore_script:\n - go get github.com/onsi/ginkgo\n - go get github.c"
},
{
"path": "LICENSE",
"chars": 1073,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 timehop\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 4016,
"preview": "# apns\n\n[](https://godoc.org/github.com/timehop/apns)\n[![B"
},
{
"path": "apns_suite_test.go",
"chars": 185,
"preview": "package apns_test\n\nimport (\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\n\t\"testing\"\n)\n\nfunc TestApns(t *test"
},
{
"path": "badge_number.go",
"chars": 1326,
"preview": "package apns\n\nimport \"encoding/json\"\n\n// BadgeNumber is much a NullInt64 in\n// database/sql except instead of using\n// t"
},
{
"path": "badge_number_test.go",
"chars": 1939,
"preview": "package apns_test\n\nimport (\n\t\"encoding/json\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\t\"github.com/timeh"
},
{
"path": "client.go",
"chars": 4206,
"preview": "package apns\n\nimport (\n\t\"container/list\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"log\"\n\t\"time\"\n)\n\ntype buffer struct {\n\tsize int\n\t*list.Lis"
},
{
"path": "client_test.go",
"chars": 9920,
"preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"time\"\n\t. \"github.com/onsi/ginkgo\"\n\t. \"githu"
},
{
"path": "conn.go",
"chars": 2040,
"preview": "package apns\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"strings\"\n)\n\nconst (\n\tProductionGateway = \"gateway.push.apple.com:2195\"\n\tSa"
},
{
"path": "conn_test.go",
"chars": 10659,
"preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\t. \""
},
{
"path": "doc.go",
"chars": 1323,
"preview": "/*\nA Go package to interface with the Apple Push\nNotification Service\n\nFeatures\n\nThis library implements a few features "
},
{
"path": "error.go",
"chars": 1547,
"preview": "package apns\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n)\n\nconst (\n\t// Error strings based on the codes specified here:\n\t// h"
},
{
"path": "error_test.go",
"chars": 2588,
"preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"math/rand\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/go"
},
{
"path": "example/example.go",
"chars": 783,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/timehop/apns\"\n)\n\nfunc main() {\n\tc, err := apns.NewClientWithFiles(apn"
},
{
"path": "feedback.go",
"chars": 1789,
"preview": "package apns\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"time\"\n)\n\ntype Feedback struct {\n\tConn"
},
{
"path": "feedback_test.go",
"chars": 4438,
"preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"time\"\n\t. \"github.com/onsi/g"
},
{
"path": "go.mod",
"chars": 480,
"preview": "module github.com/timehop/apns\n\ngo 1.20\n\nrequire (\n\tgithub.com/onsi/ginkgo v1.16.5\n\tgithub.com/onsi/gomega v1.27.6\n)\n\nre"
},
{
"path": "go.sum",
"chars": 8828,
"preview": "github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1"
},
{
"path": "notification.go",
"chars": 4687,
"preview": "package apns\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n)\n\nconst (\n"
},
{
"path": "notification_test.go",
"chars": 8651,
"preview": "package apns_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github"
}
]
About this extraction
This page contains the full source code of the timehop/apns GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (69.5 KB), approximately 24.0k tokens, and a symbol index with 100 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.