From cf7a05f80f68b5b1c8bcc0089679dd497cec2506 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Sun, 14 Jun 2015 23:53:32 -0800 Subject: first commit --- .../src/github.com/anachronistic/apns/.gitignore | 24 +++ .../src/github.com/anachronistic/apns/LICENSE | 21 ++ .../src/github.com/anachronistic/apns/README.md | 223 +++++++++++++++++++++ .../src/github.com/anachronistic/apns/client.go | 167 +++++++++++++++ .../github.com/anachronistic/apns/client_mock.go | 21 ++ .../anachronistic/apns/client_mock_test.go | 24 +++ .../src/github.com/anachronistic/apns/feedback.go | 102 ++++++++++ .../src/github.com/anachronistic/apns/legacy.go | 15 ++ .../github.com/anachronistic/apns/legacy_test.go | 27 +++ .../anachronistic/apns/mock_feedback_server.go | 53 +++++ .../anachronistic/apns/push_notification.go | 175 ++++++++++++++++ .../apns/push_notification_response.go | 36 ++++ .../anachronistic/apns/push_notification_test.go | 111 ++++++++++ 13 files changed, 999 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/README.md create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/client.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go create mode 100644 Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go (limited to 'Godeps/_workspace/src/github.com/anachronistic') diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore b/Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore new file mode 100644 index 000000000..d73587219 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/.gitignore @@ -0,0 +1,24 @@ +# 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 + +*.pem \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE b/Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE new file mode 100644 index 000000000..b80ffbd8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Alan Harris + +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. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/README.md b/Godeps/_workspace/src/github.com/anachronistic/apns/README.md new file mode 100644 index 000000000..02b012fd3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/README.md @@ -0,0 +1,223 @@ +# apns + +Utilities for Apple Push Notification and Feedback Services. + +[![GoDoc](https://godoc.org/github.com/anachronistic/apns?status.png)](https://godoc.org/github.com/anachronistic/apns) + +## Installation + +`go get github.com/anachronistic/apns` + +## Documentation + +- [APNS package documentation](http://godoc.org/github.com/anachronistic/apns) +- [Information on the APN JSON payloads](http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html) +- [Information on the APN binary protocols](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html) +- [Information on APN troubleshooting](http://developer.apple.com/library/ios/#technotes/tn2265/_index.html) + +## Usage + +### Creating pns and payloads manually +```go +package main + +import ( + "fmt" + apns "github.com/anachronistic/apns" +) + +func main() { + payload := apns.NewPayload() + payload.Alert = "Hello, world!" + payload.Badge = 42 + payload.Sound = "bingbong.aiff" + + pn := apns.NewPushNotification() + pn.AddPayload(payload) + + alert, _ := pn.PayloadString() + fmt.Println(alert) +} +``` + +#### Returns +```json +{ + "aps": { + "alert": "Hello, world!", + "badge": 42, + "sound": "bingbong.aiff" + } +} +``` + +### Using an alert dictionary for complex payloads +```go +package main + +import ( + "fmt" + apns "github.com/anachronistic/apns" +) + +func main() { + args := make([]string, 1) + args[0] = "localized args" + + dict := apns.NewAlertDictionary() + dict.Body = "Alice wants Bob to join in the fun!" + dict.ActionLocKey = "Play a Game!" + dict.LocKey = "localized key" + dict.LocArgs = args + dict.LaunchImage = "image.jpg" + + payload := apns.NewPayload() + payload.Alert = dict + payload.Badge = 42 + payload.Sound = "bingbong.aiff" + + pn := apns.NewPushNotification() + pn.AddPayload(payload) + + alert, _ := pn.PayloadString() + fmt.Println(alert) +} +``` + +#### Returns +```json +{ + "aps": { + "alert": { + "body": "Alice wants Bob to join in the fun!", + "action-loc-key": "Play a Game!", + "loc-key": "localized key", + "loc-args": [ + "localized args" + ], + "launch-image": "image.jpg" + }, + "badge": 42, + "sound": "bingbong.aiff" + } +} +``` + +### Setting custom properties +```go +package main + +import ( + "fmt" + apns "github.com/anachronistic/apns" +) + +func main() { + payload := apns.NewPayload() + payload.Alert = "Hello, world!" + payload.Badge = 42 + payload.Sound = "bingbong.aiff" + + pn := apns.NewPushNotification() + pn.AddPayload(payload) + + pn.Set("foo", "bar") + pn.Set("doctor", "who?") + pn.Set("the_ultimate_answer", 42) + + alert, _ := pn.PayloadString() + fmt.Println(alert) +} +``` + +#### Returns +```json +{ + "aps": { + "alert": "Hello, world!", + "badge": 42, + "sound": "bingbong.aiff" + }, + "doctor": "who?", + "foo": "bar", + "the_ultimate_answer": 42 +} +``` + +### Sending a notification +```go +package main + +import ( + "fmt" + apns "github.com/anachronistic/apns" +) + +func main() { + payload := apns.NewPayload() + payload.Alert = "Hello, world!" + payload.Badge = 42 + payload.Sound = "bingbong.aiff" + + pn := apns.NewPushNotification() + pn.DeviceToken = "YOUR_DEVICE_TOKEN_HERE" + pn.AddPayload(payload) + + client := apns.NewClient("gateway.sandbox.push.apple.com:2195", "YOUR_CERT_PEM", "YOUR_KEY_NOENC_PEM") + resp := client.Send(pn) + + alert, _ := pn.PayloadString() + fmt.Println(" Alert:", alert) + fmt.Println("Success:", resp.Success) + fmt.Println(" Error:", resp.Error) +} +``` + +#### Returns +```shell + Alert: {"aps":{"alert":"Hello, world!","badge":42,"sound":"bingbong.aiff"}} +Success: true + Error: +``` + +### Checking the feedback service +```go +package main + +import ( + "fmt" + apns "github.com/anachronistic/apns" + "os" +) + +func main() { + fmt.Println("- connecting to check for deactivated tokens (maximum read timeout =", apns.FeedbackTimeoutSeconds, "seconds)") + + client := apns.NewClient("feedback.sandbox.push.apple.com:2196", "YOUR_CERT_PEM", "YOUR_KEY_NOENC_PEM") + go client.ListenForFeedback() + + for { + select { + case resp := <-apns.FeedbackChannel: + fmt.Println("- recv'd:", resp.DeviceToken) + case <-apns.ShutdownChannel: + fmt.Println("- nothing returned from the feedback service") + os.Exit(1) + } + } +} +``` + +#### Returns +```shell +- connecting to check for deactivated tokens (maximum read timeout = 5 seconds) +- nothing returned from the feedback service +exit status 1 +``` + +Your output will differ if the service returns device tokens. + +```shell +- recv'd: DEVICE_TOKEN_HERE +...etc. +``` diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/client.go b/Godeps/_workspace/src/github.com/anachronistic/apns/client.go new file mode 100644 index 000000000..3fc079a87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/client.go @@ -0,0 +1,167 @@ +package apns + +import ( + "crypto/tls" + "errors" + "net" + "strings" + "time" +) + +var _ APNSClient = &Client{} + +// APNSClient is an APNS client. +type APNSClient interface { + ConnectAndWrite(resp *PushNotificationResponse, payload []byte) (err error) + Send(pn *PushNotification) (resp *PushNotificationResponse) +} + +// Client contains the fields necessary to communicate +// with Apple, such as the gateway to use and your +// certificate contents. +// +// You'll need to provide your own CertificateFile +// and KeyFile to send notifications. Ideally, you'll +// just set the CertificateFile and KeyFile fields to +// a location on drive where the certs can be loaded, +// but if you prefer you can use the CertificateBase64 +// and KeyBase64 fields to store the actual contents. +type Client struct { + Gateway string + CertificateFile string + CertificateBase64 string + KeyFile string + KeyBase64 string +} + +// BareClient can be used to set the contents of your +// certificate and key blocks manually. +func BareClient(gateway, certificateBase64, keyBase64 string) (c *Client) { + c = new(Client) + c.Gateway = gateway + c.CertificateBase64 = certificateBase64 + c.KeyBase64 = keyBase64 + return +} + +// NewClient assumes you'll be passing in paths that +// point to your certificate and key. +func NewClient(gateway, certificateFile, keyFile string) (c *Client) { + c = new(Client) + c.Gateway = gateway + c.CertificateFile = certificateFile + c.KeyFile = keyFile + return +} + +// Send connects to the APN service and sends your push notification. +// Remember that if the submission is successful, Apple won't reply. +func (client *Client) Send(pn *PushNotification) (resp *PushNotificationResponse) { + resp = new(PushNotificationResponse) + + payload, err := pn.ToBytes() + if err != nil { + resp.Success = false + resp.Error = err + return + } + + err = client.ConnectAndWrite(resp, payload) + if err != nil { + resp.Success = false + resp.Error = err + return + } + + resp.Success = true + resp.Error = nil + + return +} + +// ConnectAndWrite establishes the connection to Apple and handles the +// transmission of your push notification, as well as waiting for a reply. +// +// In lieu of a timeout (which would be available in Go 1.1) +// we use a timeout channel pattern instead. We start two goroutines, +// one of which just sleeps for TimeoutSeconds seconds, while the other +// waits for a response from the Apple servers. +// +// Whichever channel puts data on first is the "winner". As such, it's +// possible to get a false positive if Apple takes a long time to respond. +// It's probably not a deal-breaker, but something to be aware of. +func (client *Client) ConnectAndWrite(resp *PushNotificationResponse, payload []byte) (err error) { + var cert tls.Certificate + + if len(client.CertificateBase64) == 0 && len(client.KeyBase64) == 0 { + // The user did not specify raw block contents, so check the filesystem. + cert, err = tls.LoadX509KeyPair(client.CertificateFile, client.KeyFile) + } else { + // The user provided the raw block contents, so use that. + cert, err = tls.X509KeyPair([]byte(client.CertificateBase64), []byte(client.KeyBase64)) + } + + if err != nil { + return err + } + + gatewayParts := strings.Split(client.Gateway, ":") + conf := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: gatewayParts[0], + } + + conn, err := net.Dial("tcp", client.Gateway) + if err != nil { + return err + } + defer conn.Close() + + tlsConn := tls.Client(conn, conf) + err = tlsConn.Handshake() + if err != nil { + return err + } + defer tlsConn.Close() + + _, err = tlsConn.Write(payload) + if err != nil { + return err + } + + // Create one channel that will serve to handle + // timeouts when the notification succeeds. + timeoutChannel := make(chan bool, 1) + go func() { + time.Sleep(time.Second * TimeoutSeconds) + timeoutChannel <- true + }() + + // This channel will contain the binary response + // from Apple in the event of a failure. + responseChannel := make(chan []byte, 1) + go func() { + buffer := make([]byte, 6, 6) + tlsConn.Read(buffer) + responseChannel <- buffer + }() + + // First one back wins! + // The data structure for an APN response is as follows: + // + // command -> 1 byte + // status -> 1 byte + // identifier -> 4 bytes + // + // The first byte will always be set to 8. + select { + case r := <-responseChannel: + resp.Success = false + resp.AppleResponse = ApplePushResponses[r[1]] + err = errors.New(resp.AppleResponse) + case <-timeoutChannel: + resp.Success = true + } + + return err +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go new file mode 100644 index 000000000..29a1f4b23 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock.go @@ -0,0 +1,21 @@ +package apns + +import "github.com/stretchr/testify/mock" + +type MockClient struct { + mock.Mock +} + +func (m *MockClient) ConnectAndWrite(resp *PushNotificationResponse, payload []byte) (err error) { + return m.Called(resp, payload).Error(0) +} + +func (m *MockClient) Send(pn *PushNotification) (resp *PushNotificationResponse) { + r := m.Called(pn).Get(0) + if r != nil { + if r, ok := r.(*PushNotificationResponse); ok { + return r + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go new file mode 100644 index 000000000..86e997b5a --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/client_mock_test.go @@ -0,0 +1,24 @@ +package apns + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMockClientConnectAndWrite(t *testing.T) { + m := &MockClient{} + m.On("ConnectAndWrite", (*PushNotificationResponse)(nil), []byte(nil)).Return(nil) + assert.Nil(t, m.ConnectAndWrite(nil, nil)) + m.On("ConnectAndWrite", &PushNotificationResponse{}, []byte{}).Return(errors.New("test")) + assert.Equal(t, errors.New("test"), m.ConnectAndWrite(&PushNotificationResponse{}, []byte{})) +} + +func TestMockClientSend(t *testing.T) { + m := &MockClient{} + m.On("Send", (*PushNotification)(nil)).Return(nil) + assert.Nil(t, m.Send(nil)) + m.On("Send", &PushNotification{}).Return(&PushNotificationResponse{}) + assert.Equal(t, &PushNotificationResponse{}, m.Send(&PushNotification{})) +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go b/Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go new file mode 100644 index 000000000..32e7f0f15 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/feedback.go @@ -0,0 +1,102 @@ +package apns + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "encoding/hex" + "errors" + "net" + "strings" + "time" +) + +// Wait at most this many seconds for feedback data from Apple. +const FeedbackTimeoutSeconds = 5 + +// FeedbackChannel will receive individual responses from Apple. +var FeedbackChannel = make(chan (*FeedbackResponse)) + +// If there's nothing to read, ShutdownChannel gets a true. +var ShutdownChannel = make(chan bool) + +// FeedbackResponse represents a device token that Apple has +// indicated should not be sent to in the future. +type FeedbackResponse struct { + Timestamp uint32 + DeviceToken string +} + +// NewFeedbackResponse creates and returns a FeedbackResponse structure. +func NewFeedbackResponse() (resp *FeedbackResponse) { + resp = new(FeedbackResponse) + return +} + +// ListenForFeedback connects to the Apple Feedback Service +// and checks for device tokens. +// +// Feedback consists of device tokens that should +// not be sent to in the future; Apple *does* monitor that +// you respect this so you should be checking it ;) +func (client *Client) ListenForFeedback() (err error) { + var cert tls.Certificate + + if len(client.CertificateBase64) == 0 && len(client.KeyBase64) == 0 { + // The user did not specify raw block contents, so check the filesystem. + cert, err = tls.LoadX509KeyPair(client.CertificateFile, client.KeyFile) + } else { + // The user provided the raw block contents, so use that. + cert, err = tls.X509KeyPair([]byte(client.CertificateBase64), []byte(client.KeyBase64)) + } + + if err != nil { + return err + } + + gatewayParts := strings.Split(client.Gateway, ":") + conf := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: gatewayParts[0], + } + + conn, err := net.Dial("tcp", client.Gateway) + if err != nil { + return err + } + defer conn.Close() + conn.SetReadDeadline(time.Now().Add(FeedbackTimeoutSeconds * time.Second)) + + tlsConn := tls.Client(conn, conf) + err = tlsConn.Handshake() + if err != nil { + return err + } + + var tokenLength uint16 + buffer := make([]byte, 38, 38) + deviceToken := make([]byte, 32, 32) + + for { + _, err := tlsConn.Read(buffer) + if err != nil { + ShutdownChannel <- true + break + } + + resp := NewFeedbackResponse() + + r := bytes.NewReader(buffer) + binary.Read(r, binary.BigEndian, &resp.Timestamp) + binary.Read(r, binary.BigEndian, &tokenLength) + binary.Read(r, binary.BigEndian, &deviceToken) + if tokenLength != 32 { + return errors.New("token length should be equal to 32, but isn't") + } + resp.DeviceToken = hex.EncodeToString(deviceToken) + + FeedbackChannel <- resp + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go new file mode 100644 index 000000000..44da3dde8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy.go @@ -0,0 +1,15 @@ +package apns + +// This file exists to support backward-compatibility +// as I gradually refactor and overhaul. Ideally, golint +// should only complain about this file (and we should +// try to keep its complaints to a minimum). + +// These variables map old identifiers to their current format. +var ( + APPLE_PUSH_RESPONSES = ApplePushResponses + FEEDBACK_TIMEOUT_SECONDS = FeedbackTimeoutSeconds + IDENTIFIER_UBOUND = IdentifierUbound + MAX_PAYLOAD_SIZE_BYTES = MaxPayloadSizeBytes + TIMEOUT_SECONDS = TimeoutSeconds +) diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go new file mode 100644 index 000000000..4b983c128 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/legacy_test.go @@ -0,0 +1,27 @@ +package apns + +import ( + "reflect" + "testing" +) + +// These identifiers were changed to resolve golint violations. +// However, it's possible that legacy code may rely on them. This +// will help avoid springing a breaking change on people. +func TestLegacyConstants(t *testing.T) { + if !reflect.DeepEqual(APPLE_PUSH_RESPONSES, ApplePushResponses) { + t.Error("expected APPLE_PUSH_RESPONSES to equal ApplePushResponses") + } + if !reflect.DeepEqual(FEEDBACK_TIMEOUT_SECONDS, FeedbackTimeoutSeconds) { + t.Error("expected FEEDBACK_TIMEOUT_SECONDS to equal FeedbackTimeoutSeconds") + } + if !reflect.DeepEqual(IDENTIFIER_UBOUND, IdentifierUbound) { + t.Error("expected IDENTIFIER_UBOUND to equal IdentifierUbound") + } + if !reflect.DeepEqual(MAX_PAYLOAD_SIZE_BYTES, MaxPayloadSizeBytes) { + t.Error("expected MAX_PAYLOAD_SIZE_BYTES to equal MaxPayloadSizeBytes") + } + if !reflect.DeepEqual(TIMEOUT_SECONDS, TimeoutSeconds) { + t.Error("expected TIMEOUT_SECONDS to equal TimeoutSeconds") + } +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go b/Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go new file mode 100644 index 000000000..d7536f261 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/mock_feedback_server.go @@ -0,0 +1,53 @@ +package apns + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "log" + "net" + "time" +) + +// StartMockFeedbackServer spins up a simple stand-in for the Apple +// feedback service that can be used for testing purposes. Doesn't +// handle many errors, etc. Just for the sake of having something "live" +// to hit. +func StartMockFeedbackServer(certFile, keyFile string) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + log.Panic(err) + } + config := tls.Config{Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAnyClientCert} + log.Print("- starting Mock Apple Feedback TCP server at 0.0.0.0:5555") + + srv, _ := tls.Listen("tcp", "0.0.0.0:5555", &config) + for { + conn, err := srv.Accept() + if err != nil { + log.Panic(err) + } + go loop(conn) + } +} + +// Writes binary data to the client in the same +// manner as the Apple service would. +// +// [4 bytes, 2 bytes, 32 bytes] = 38 bytes total +func loop(conn net.Conn) { + defer conn.Close() + for { + timeT := uint32(1368809290) // 2013-05-17 12:48:10 -0400 + token := "abcd1234efab5678abcd1234efab5678" + + buf := new(bytes.Buffer) + binary.Write(buf, binary.BigEndian, timeT) + binary.Write(buf, binary.BigEndian, uint16(len(token))) + binary.Write(buf, binary.BigEndian, []byte(token)) + conn.Write(buf.Bytes()) + + dur, _ := time.ParseDuration("1s") + time.Sleep(dur) + } +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go new file mode 100644 index 000000000..e6b58d575 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification.go @@ -0,0 +1,175 @@ +package apns + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "math/rand" + "strconv" + "time" +) + +// Push commands always start with command value 2. +const pushCommandValue = 2 + +// Your total notification payload cannot exceed 2 KB. +const MaxPayloadSizeBytes = 2048 + +// Every push notification gets a pseudo-unique identifier; +// this establishes the upper boundary for it. Apple will return +// this identifier if there is an issue sending your notification. +const IdentifierUbound = 9999 + +// Constants related to the payload fields and their lengths. +const ( + deviceTokenItemid = 1 + payloadItemid = 2 + notificationIdentifierItemid = 3 + expirationDateItemid = 4 + priorityItemid = 5 + deviceTokenLength = 32 + notificationIdentifierLength = 4 + expirationDateLength = 4 + priorityLength = 1 +) + +// Payload contains the notification data for your request. +// +// Alert is an interface here because it supports either a string +// or a dictionary, represented within by an AlertDictionary struct. +type Payload struct { + Alert interface{} `json:"alert,omitempty"` + Badge int `json:"badge,omitempty"` + Sound string `json:"sound,omitempty"` + ContentAvailable int `json:"content-available,omitempty"` + Category string `json:"category,omitempty"` +} + +// NewPayload creates and returns a Payload structure. +func NewPayload() *Payload { + return new(Payload) +} + +// AlertDictionary is a more complex notification payload. +// +// From the APN docs: "Use the ... alert dictionary in general only if you absolutely need to." +// The AlertDictionary is suitable for specific localization needs. +type AlertDictionary struct { + Body string `json:"body,omitempty"` + ActionLocKey string `json:"action-loc-key,omitempty"` + LocKey string `json:"loc-key,omitempty"` + LocArgs []string `json:"loc-args,omitempty"` + LaunchImage string `json:"launch-image,omitempty"` +} + +// NewAlertDictionary creates and returns an AlertDictionary structure. +func NewAlertDictionary() *AlertDictionary { + return new(AlertDictionary) +} + +// PushNotification is the wrapper for the Payload. +// The length fields are computed in ToBytes() and aren't represented here. +type PushNotification struct { + Identifier int32 + Expiry uint32 + DeviceToken string + payload map[string]interface{} + Priority uint8 +} + +// NewPushNotification creates and returns a PushNotification structure. +// It also initializes the pseudo-random identifier. +func NewPushNotification() (pn *PushNotification) { + pn = new(PushNotification) + pn.payload = make(map[string]interface{}) + pn.Identifier = rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(IdentifierUbound) + pn.Priority = 10 + return +} + +// AddPayload sets the "aps" payload section of the request. It also +// has a hack described within to deal with specific zero values. +func (pn *PushNotification) AddPayload(p *Payload) { + // This deserves some explanation. + // + // Setting an exported field of type int to 0 + // triggers the omitempty behavior if you've set it. + // Since the badge is optional, we should omit it if + // it's not set. However, we want to include it if the + // value is 0, so there's a hack in push_notification.go + // that exploits the fact that Apple treats -1 for a + // badge value as though it were 0 (i.e. it clears the + // badge but doesn't stop the notification from going + // through successfully.) + // + // Still a hack though :) + if p.Badge == 0 { + p.Badge = -1 + } + pn.Set("aps", p) +} + +// Get returns the value of a payload key, if it exists. +func (pn *PushNotification) Get(key string) interface{} { + return pn.payload[key] +} + +// Set defines the value of a payload key. +func (pn *PushNotification) Set(key string, value interface{}) { + pn.payload[key] = value +} + +// PayloadJSON returns the current payload in JSON format. +func (pn *PushNotification) PayloadJSON() ([]byte, error) { + return json.Marshal(pn.payload) +} + +// PayloadString returns the current payload in string format. +func (pn *PushNotification) PayloadString() (string, error) { + j, err := pn.PayloadJSON() + return string(j), err +} + +// ToBytes returns a byte array of the complete PushNotification +// struct. This array is what should be transmitted to the APN Service. +func (pn *PushNotification) ToBytes() ([]byte, error) { + token, err := hex.DecodeString(pn.DeviceToken) + if err != nil { + return nil, err + } + if len(token) != deviceTokenLength { + return nil, errors.New("device token has incorrect length") + } + payload, err := pn.PayloadJSON() + if err != nil { + return nil, err + } + if len(payload) > MaxPayloadSizeBytes { + return nil, errors.New("payload is larger than the " + strconv.Itoa(MaxPayloadSizeBytes) + " byte limit") + } + + frameBuffer := new(bytes.Buffer) + binary.Write(frameBuffer, binary.BigEndian, uint8(deviceTokenItemid)) + binary.Write(frameBuffer, binary.BigEndian, uint16(deviceTokenLength)) + binary.Write(frameBuffer, binary.BigEndian, token) + binary.Write(frameBuffer, binary.BigEndian, uint8(payloadItemid)) + binary.Write(frameBuffer, binary.BigEndian, uint16(len(payload))) + binary.Write(frameBuffer, binary.BigEndian, payload) + binary.Write(frameBuffer, binary.BigEndian, uint8(notificationIdentifierItemid)) + binary.Write(frameBuffer, binary.BigEndian, uint16(notificationIdentifierLength)) + binary.Write(frameBuffer, binary.BigEndian, pn.Identifier) + binary.Write(frameBuffer, binary.BigEndian, uint8(expirationDateItemid)) + binary.Write(frameBuffer, binary.BigEndian, uint16(expirationDateLength)) + binary.Write(frameBuffer, binary.BigEndian, pn.Expiry) + binary.Write(frameBuffer, binary.BigEndian, uint8(priorityItemid)) + binary.Write(frameBuffer, binary.BigEndian, uint16(priorityLength)) + binary.Write(frameBuffer, binary.BigEndian, pn.Priority) + + buffer := bytes.NewBuffer([]byte{}) + binary.Write(buffer, binary.BigEndian, uint8(pushCommandValue)) + binary.Write(buffer, binary.BigEndian, uint32(frameBuffer.Len())) + binary.Write(buffer, binary.BigEndian, frameBuffer.Bytes()) + return buffer.Bytes(), nil +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go new file mode 100644 index 000000000..f08dc06e4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_response.go @@ -0,0 +1,36 @@ +package apns + +// The maximum number of seconds we're willing to wait for a response +// from the Apple Push Notification Service. +const TimeoutSeconds = 5 + +// This enumerates the response codes that Apple defines +// for push notification attempts. +var ApplePushResponses = map[uint8]string{ + 0: "NO_ERRORS", + 1: "PROCESSING_ERROR", + 2: "MISSING_DEVICE_TOKEN", + 3: "MISSING_TOPIC", + 4: "MISSING_PAYLOAD", + 5: "INVALID_TOKEN_SIZE", + 6: "INVALID_TOPIC_SIZE", + 7: "INVALID_PAYLOAD_SIZE", + 8: "INVALID_TOKEN", + 10: "SHUTDOWN", + 255: "UNKNOWN", +} + +// PushNotificationResponse details what Apple had to say, if anything. +type PushNotificationResponse struct { + Success bool + AppleResponse string + Error error +} + +// NewPushNotificationResponse creates and returns a new PushNotificationResponse +// structure; it defaults to being unsuccessful at first. +func NewPushNotificationResponse() (resp *PushNotificationResponse) { + resp = new(PushNotificationResponse) + resp.Success = false + return +} diff --git a/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go new file mode 100644 index 000000000..a17b1c833 --- /dev/null +++ b/Godeps/_workspace/src/github.com/anachronistic/apns/push_notification_test.go @@ -0,0 +1,111 @@ +package apns + +import ( + "testing" +) + +const testDeviceToken = "e93b7686988b4b5fd334298e60e73d90035f6d12628a80b4029bde0dec514df9" + +// Create a new Payload that specifies simple text, +// a badge counter, and a custom notification sound. +func mockPayload() (payload *Payload) { + payload = NewPayload() + payload.Alert = "You have mail!" + payload.Badge = 42 + payload.Sound = "bingbong.aiff" + return +} + +// See the commentary in push_notification.go for information +// on why we're testing a badge of value 0. +func mockZeroBadgePayload() (payload *Payload) { + payload = mockPayload() + payload.Badge = 0 + return +} + +// Create a new AlertDictionary. Apple recommends you not use +// the more complex alert style unless absolutely necessary. +func mockAlertDictionary() (dict *AlertDictionary) { + args := make([]string, 1) + args[0] = "localized args" + + dict = NewAlertDictionary() + dict.Body = "Complex Message" + dict.ActionLocKey = "Play a Game!" + dict.LocKey = "localized key" + dict.LocArgs = args + dict.LaunchImage = "image.jpg" + return +} + +func TestBasicAlert(t *testing.T) { + payload := mockPayload() + pn := NewPushNotification() + + pn.DeviceToken = testDeviceToken + pn.AddPayload(payload) + + bytes, _ := pn.ToBytes() + json, _ := pn.PayloadJSON() + if len(bytes) != 130 { + t.Error("expected 130 bytes; got", len(bytes)) + } + if len(json) != 69 { + t.Error("expected 69 bytes; got", len(json)) + } +} + +func TestAlertDictionary(t *testing.T) { + dict := mockAlertDictionary() + payload := mockPayload() + payload.Alert = dict + + pn := NewPushNotification() + pn.DeviceToken = testDeviceToken + pn.AddPayload(payload) + + bytes, _ := pn.ToBytes() + json, _ := pn.PayloadJSON() + if len(bytes) != 255 { + t.Error("expected 255 bytes; got", len(bytes)) + } + if len(json) != 194 { + t.Error("expected 194 bytes; got", len(bytes)) + } +} + +func TestCustomParameters(t *testing.T) { + payload := mockPayload() + pn := NewPushNotification() + + pn.DeviceToken = testDeviceToken + pn.AddPayload(payload) + pn.Set("foo", "bar") + + if pn.Get("foo") != "bar" { + t.Error("unable to set a custom property") + } + if pn.Get("not_set") != nil { + t.Error("expected a missing key to return nil") + } + + bytes, _ := pn.ToBytes() + json, _ := pn.PayloadJSON() + if len(bytes) != 142 { + t.Error("expected 110 bytes; got", len(bytes)) + } + if len(json) != 81 { + t.Error("expected 81 bytes; got", len(json)) + } +} + +func TestZeroBadgeChangesToNegativeOne(t *testing.T) { + payload := mockZeroBadgePayload() + pn := NewPushNotification() + pn.AddPayload(payload) + + if payload.Badge != -1 { + t.Error("expected 0 badge value to be converted to -1; got", payload.Badge) + } +} -- cgit v1.2.3-1-g7c22