├── .DS_Store ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _examples ├── .DS_Store ├── basic │ └── main.go ├── channels │ └── main.go ├── pem │ └── main.go ├── pipeline │ ├── .DS_Store │ └── main.go └── pipeline_pem │ └── main.go ├── alert.go ├── alert_test.go ├── apns_response.go ├── certificate └── certificate.go ├── client.go ├── client_test.go ├── constants.go ├── error_codes.go ├── error_response.go ├── headers.go ├── headers_test.go ├── payload.go ├── payload_test.go └── status_codes.go /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sger/go-apns2/e18d1733c2a01b73d8c207d5f41ad54004caf1b1/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | _examples/certs 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6.x 5 | - 1.7.x 6 | - 1.8.x 7 | - tip 8 | 9 | before_install: 10 | - go get github.com/axw/gocov/gocov 11 | - go get github.com/mattn/goveralls 12 | - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 13 | 14 | install: 15 | - go get golang.org/x/net/http2 16 | - go get golang.org/x/net/context 17 | - go get golang.org/x/crypto/pkcs12 18 | 19 | os: 20 | - linux 21 | 22 | script: 23 | - go test -race -v ./... 24 | - $HOME/gopath/bin/goveralls -service=travis-ci 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 GO APNS2 contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sger/go-apns2.svg?branch=master)](https://travis-ci.org/sger/go-apns2) 2 | [![GoDoc](https://godoc.org/github.com/sger/go-apns2?status.svg)](https://godoc.org/github.com/sger/go-apns2) 3 | [![Coverage Status](https://coveralls.io/repos/github/sger/go-apns2/badge.svg?branch=master)](https://coveralls.io/github/sger/go-apns2?branch=master) 4 | # Go Apns2 5 | 6 | Go package for HTTP/2 [Apple Push Notification Service](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html). 7 | 8 | ## Installation 9 | 10 | Via `go-get`: 11 | 12 | ```sh 13 | $ go get github.com/sger/go-apns2 14 | $ cd go-apns2/_examples 15 | $ cd basic 16 | $ go build 17 | $ ./basic 18 | ``` 19 | 20 | ## Documentation 21 | 22 | ```sh 23 | $ godoc . 24 | $ godoc -http=:6060 25 | ``` 26 | 27 | ## Usage 28 | 29 | ## Simple example 30 | 31 | ```go 32 | package main 33 | 34 | import ( 35 | "fmt" 36 | "log" 37 | 38 | "github.com/sger/go-apns2" 39 | "github.com/sger/go-apns2/certificate" 40 | ) 41 | 42 | func main() { 43 | var deviceToken = "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 44 | var filename = "../certs/PushChatKey.p12" 45 | var password = "pushchat" 46 | 47 | // Setup payload must contains an aps root label and alert message 48 | payload := apns2.Payload{ 49 | Alert: apns2.Alert{ 50 | Body: "Testing HTTP 2"}, 51 | Badge: 5, 52 | } 53 | 54 | // Parse the certificate 55 | cert, err := certificate.ReadP12File(filename, password) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | // Setup a new http client with pass the Certificate 61 | // and host environment (apns2.Development, apns2.Production) 62 | client, err := apns2.NewClient(cert, apns2.Development) 63 | 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | // Send the Push Notification 69 | resp, err := client.SendPush(payload, deviceToken, &apns2.Headers{}) 70 | 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | // Returns ApnsResponse struct 76 | /* 77 | type ApnsResponse struct { 78 | StatusCode int 79 | StatusCodeDescription string 80 | ApnsID string `json:"apns-id,omitempty"` 81 | Reason string `json:"reason,omitempty"` 82 | }*/ 83 | fmt.Println(resp) 84 | } 85 | ``` 86 | 87 | ## Goroutines and channels example 88 | 89 | ```go 90 | package main 91 | 92 | import ( 93 | "fmt" 94 | "log" 95 | "time" 96 | 97 | "github.com/sger/go-apns2" 98 | "github.com/sger/go-apns2/certificate" 99 | ) 100 | 101 | var status bool 102 | var payloads []apns2.Payload 103 | var payloadsProcessed int 104 | var totalPayloads int 105 | var apns []*apns2.ApnsResponse 106 | 107 | func main() { 108 | status = true 109 | statusChannel := make(chan int) 110 | payloadChannel := make(chan *apns2.ApnsResponse) 111 | totalPayloads = 0 112 | 113 | // Creating 1000 payloads 114 | for i := 0; i < 1000; i++ { 115 | message := fmt.Sprintf("Hello World %v!", i) 116 | payload := apns2.Payload{ 117 | Alert: apns2.Alert{ 118 | Body: message}, 119 | } 120 | payloads = append(payloads, payload) 121 | } 122 | 123 | payloadsProcessed = 0 124 | totalPayloads = len(payloads) 125 | 126 | // goroutines 127 | go sendPayloads(statusChannel, payloadChannel) 128 | go processPayloadResponses(payloadChannel) 129 | 130 | for { 131 | if status == false { 132 | for _, id := range apns { 133 | fmt.Println(id) 134 | } 135 | fmt.Println("Done sending ", totalPayloads, " payloads") 136 | break 137 | } 138 | select { 139 | case sC := <-statusChannel: 140 | fmt.Println("Payload received on StatusChannel", sC) 141 | payloadsProcessed++ 142 | if payloadsProcessed == totalPayloads { 143 | fmt.Println("Received all Payloads") 144 | status = false 145 | close(statusChannel) 146 | close(payloadChannel) 147 | } 148 | } 149 | } 150 | } 151 | 152 | func sendPayloads(statusChannel chan int, payloadChannel chan *apns2.ApnsResponse) { 153 | time.Sleep(time.Millisecond * 1) 154 | fmt.Println("Sending", len(payloads), "payloads") 155 | 156 | var deviceToken = "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 157 | var filename = "../certs/PushChatKey.p12" 158 | var password = "pushchat" 159 | 160 | cert, err := certificate.ReadP12File(filename, password) 161 | if err != nil { 162 | log.Fatal(err) 163 | } 164 | 165 | // Setup a new http client 166 | client, err := apns2.NewClient(cert, apns2.Development) 167 | 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | 172 | for i := 0; i < totalPayloads; i++ { 173 | fmt.Println("sending payload ", i, payloads[i]) 174 | resp, err := client.SendPush(payloads[i], deviceToken, &apns2.Headers{}) 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | payloadChannel <- resp 179 | statusChannel <- 0 180 | } 181 | } 182 | 183 | func processPayloadResponses(payloadChannel chan *apns2.ApnsResponse) { 184 | for { 185 | select { 186 | case pC := <-payloadChannel: 187 | apns = append(apns, pC) 188 | } 189 | } 190 | } 191 | ``` 192 | 193 | ## TODO 194 | - [x] Pem Support 195 | - [x] Tests 196 | - [x] Error Handling 197 | - [x] Support for Feedback service 198 | 199 | Author 200 | ----- 201 | 202 | __Spiros Gerokostas__ 203 | 204 | - [![](https://img.shields.io/badge/twitter-sger-brightgreen.svg)](https://twitter.com/sger) 205 | - :email: spiros.gerokostas@gmail.com 206 | 207 | License 208 | ----- 209 | 210 | Go Apns2 is available under the MIT license. See the LICENSE file for more info. 211 | 212 | -------------------------------------------------------------------------------- /_examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sger/go-apns2/e18d1733c2a01b73d8c207d5f41ad54004caf1b1/_examples/.DS_Store -------------------------------------------------------------------------------- /_examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/sger/go-apns2" 8 | "github.com/sger/go-apns2/certificate" 9 | ) 10 | 11 | func main() { 12 | var deviceToken = "734f6b32a73409c5ae05129cdcbbdaaad188ebedfe2692f0e048a5887614ade8" 13 | var filename = "../certs/GoPushNotifications.p12" 14 | var password = "GoPushNotifications" 15 | 16 | // Setup payload must contains an aps root label and alert message 17 | payload := apns2.Payload{ 18 | Alert: apns2.Alert{ 19 | Body: "Testing HTTP 2"}, 20 | Badge: 5, 21 | } 22 | 23 | // Parse the certificate 24 | cert, err := certificate.ReadP12File(filename, password) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | // Setup a new http client with pass the Certificate 30 | // and host environment (apns2.Development, apns2.Production) 31 | client, err := apns2.NewClient(cert, apns2.Development) 32 | 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | // Send the Push Notification 38 | resp, err := client.SendPush(payload, deviceToken, &apns2.Headers{}) 39 | 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | // Returns ApnsResponse struct 45 | /* 46 | type ApnsResponse struct { 47 | StatusCode int 48 | StatusCodeDescription string 49 | ApnsID string `json:"apns-id,omitempty"` 50 | Reason string `json:"reason,omitempty"` 51 | }*/ 52 | fmt.Println(resp) 53 | } 54 | -------------------------------------------------------------------------------- /_examples/channels/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/sger/go-apns2" 9 | "github.com/sger/go-apns2/certificate" 10 | ) 11 | 12 | func main() { 13 | 14 | payloads := []apns2.Payload{} 15 | 16 | for i := 0; i < 200; i++ { 17 | message := fmt.Sprintf("Hello World %v!", i) 18 | payload := apns2.Payload{ 19 | Alert: apns2.Alert{ 20 | Body: message}, 21 | } 22 | payloads = append(payloads, payload) 23 | } 24 | 25 | results := asyncHTTPPosts(payloads) 26 | 27 | for _, result := range results { 28 | if result != nil { 29 | fmt.Println(result) 30 | } 31 | } 32 | } 33 | 34 | func asyncHTTPPosts(payloads []apns2.Payload) []*apns2.ApnsResponse { 35 | 36 | var deviceToken = "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 37 | var filename = "../certs/PushChatKey.p12" 38 | var password = "pushchat" 39 | 40 | ch := make(chan *apns2.ApnsResponse) 41 | responses := []*apns2.ApnsResponse{} 42 | 43 | cert, err := certificate.ReadP12File(filename, password) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // Setup a new http client 49 | client, err := apns2.NewClient(cert, apns2.Development) 50 | 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | for _, payload := range payloads { 56 | go func(payload apns2.Payload) { 57 | fmt.Printf("Sending %v \n", payload) 58 | resp, err := client.SendPush(payload, deviceToken, &apns2.Headers{}) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | ch <- resp 63 | }(payload) 64 | } 65 | 66 | for { 67 | select { 68 | case resp := <-ch: 69 | fmt.Printf("%v was received \n", resp) 70 | responses = append(responses, resp) 71 | if len(responses) == len(payloads) { 72 | return responses 73 | } 74 | case <-time.After(50 * time.Millisecond): 75 | fmt.Printf(".") 76 | } 77 | } 78 | return responses 79 | } 80 | -------------------------------------------------------------------------------- /_examples/pem/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/sger/go-apns2" 8 | "github.com/sger/go-apns2/certificate" 9 | ) 10 | 11 | func main() { 12 | var deviceToken = "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 13 | var pemFilename = "../certs/ck.pem" 14 | var password = "pushchat" 15 | 16 | cert, err := certificate.ReadPemFile(pemFilename, password) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | // Setup payload must contains an aps root label and alert message 22 | payload := apns2.Payload{ 23 | Alert: apns2.Alert{ 24 | Body: "Testing HTTP 2"}, 25 | Badge: 5, 26 | } 27 | 28 | // Setup a new http client with pass the Certificate 29 | // and host environment (apns2.Development, apns2.Production) 30 | client, err := apns2.NewClient(cert, apns2.Development) 31 | 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | // Send the Push Notification 37 | resp, err := client.SendPush(payload, deviceToken, &apns2.Headers{}) 38 | 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // Returns ApnsResponse struct 44 | /* 45 | type ApnsResponse struct { 46 | StatusCode int 47 | StatusCodeDescription string 48 | ApnsID string `json:"apns-id,omitempty"` 49 | Reason string `json:"reason,omitempty"` 50 | }*/ 51 | fmt.Println(resp) 52 | 53 | } 54 | -------------------------------------------------------------------------------- /_examples/pipeline/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sger/go-apns2/e18d1733c2a01b73d8c207d5f41ad54004caf1b1/_examples/pipeline/.DS_Store -------------------------------------------------------------------------------- /_examples/pipeline/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/sger/go-apns2" 9 | "github.com/sger/go-apns2/certificate" 10 | ) 11 | 12 | var status bool 13 | var payloads []apns2.Payload 14 | var payloadsProcessed int 15 | var totalPayloads int 16 | var apns []*apns2.ApnsResponse 17 | 18 | func main() { 19 | status = true 20 | statusChannel := make(chan int) 21 | payloadChannel := make(chan *apns2.ApnsResponse) 22 | totalPayloads = 0 23 | 24 | // Creating 1000 payloads 25 | for i := 0; i < 1000; i++ { 26 | message := fmt.Sprintf("Hello World %v!", i) 27 | payload := apns2.Payload{ 28 | Alert: apns2.Alert{ 29 | Body: message}, 30 | } 31 | payloads = append(payloads, payload) 32 | } 33 | 34 | payloadsProcessed = 0 35 | totalPayloads = len(payloads) 36 | 37 | // goroutines 38 | go sendPayloads(statusChannel, payloadChannel) 39 | go processPayloadResponses(payloadChannel) 40 | 41 | for { 42 | if status == false { 43 | for _, id := range apns { 44 | fmt.Println(id) 45 | } 46 | fmt.Println("Done sending ", totalPayloads, " payloads") 47 | break 48 | } 49 | select { 50 | case sC := <-statusChannel: 51 | fmt.Println("Payload received on StatusChannel", sC) 52 | payloadsProcessed++ 53 | if payloadsProcessed == totalPayloads { 54 | fmt.Println("Received all Payloads") 55 | status = false 56 | close(statusChannel) 57 | close(payloadChannel) 58 | } 59 | } 60 | } 61 | } 62 | 63 | func sendPayloads(statusChannel chan int, payloadChannel chan *apns2.ApnsResponse) { 64 | time.Sleep(time.Millisecond * 1) 65 | fmt.Println("Sending", len(payloads), "payloads") 66 | 67 | var deviceToken = "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 68 | var filename = "../certs/PushChatKey.p12" 69 | var password = "pushchat" 70 | 71 | cert, err := certificate.ReadP12File(filename, password) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | // Setup a new http client 77 | client, err := apns2.NewClient(cert, apns2.Development) 78 | 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | for i := 0; i < totalPayloads; i++ { 84 | fmt.Println("sending payload ", i, payloads[i]) 85 | resp, err := client.SendPush(payloads[i], deviceToken, &apns2.Headers{}) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | payloadChannel <- resp 90 | statusChannel <- 0 91 | } 92 | } 93 | 94 | func processPayloadResponses(payloadChannel chan *apns2.ApnsResponse) { 95 | for { 96 | select { 97 | case pC := <-payloadChannel: 98 | apns = append(apns, pC) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /_examples/pipeline_pem/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/sger/go-apns2" 9 | "github.com/sger/go-apns2/certificate" 10 | ) 11 | 12 | var status bool 13 | var payloads []apns2.Payload 14 | var payloadsProcessed int 15 | var totalPayloads int 16 | var apns []*apns2.ApnsResponse 17 | 18 | func main() { 19 | status = true 20 | statusChannel := make(chan int) 21 | payloadChannel := make(chan *apns2.ApnsResponse) 22 | totalPayloads = 0 23 | 24 | // Creating 2000 payloads 25 | for i := 0; i < 2000; i++ { 26 | message := fmt.Sprintf("Hello World %v!", i) 27 | payload := apns2.Payload{ 28 | Alert: apns2.Alert{ 29 | Body: message}, 30 | } 31 | payloads = append(payloads, payload) 32 | } 33 | 34 | payloadsProcessed = 0 35 | totalPayloads = len(payloads) 36 | 37 | // goroutines 38 | go sendPayloads(statusChannel, payloadChannel) 39 | go processPayloadResponses(payloadChannel) 40 | 41 | for { 42 | if status == false { 43 | for _, id := range apns { 44 | fmt.Println(id) 45 | } 46 | fmt.Println("Done sending ", totalPayloads, " payloads") 47 | break 48 | } 49 | select { 50 | case sC := <-statusChannel: 51 | fmt.Println("Payload received on StatusChannel", sC) 52 | payloadsProcessed++ 53 | if payloadsProcessed == totalPayloads { 54 | fmt.Println("Received all Payloads") 55 | status = false 56 | close(statusChannel) 57 | close(payloadChannel) 58 | } 59 | } 60 | } 61 | } 62 | 63 | func sendPayloads(statusChannel chan int, payloadChannel chan *apns2.ApnsResponse) { 64 | time.Sleep(time.Millisecond * 1) 65 | fmt.Println("Sending", len(payloads), "payloads") 66 | 67 | var deviceToken = "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 68 | var pemFilename = "../certs/ck.pem" 69 | var password = "pushchat" 70 | 71 | cert, err := certificate.ReadPemFile(pemFilename, password) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | // Setup a new http client 77 | client, err := apns2.NewClient(cert, apns2.Development) 78 | 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | 83 | for i := 0; i < totalPayloads; i++ { 84 | fmt.Println("sending payload ", i, payloads[i]) 85 | resp, err := client.SendPush(payloads[i], deviceToken, &apns2.Headers{}) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | payloadChannel <- resp 90 | statusChannel <- 0 91 | } 92 | } 93 | 94 | func processPayloadResponses(payloadChannel chan *apns2.ApnsResponse) { 95 | for { 96 | select { 97 | case pC := <-payloadChannel: 98 | apns = append(apns, pC) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /alert.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | // Alert If this property is included, the system displays a standard alert or a banner, based 4 | // on the user’s setting. You can specify a string or a dictionary as the value of alert. 5 | type Alert struct { 6 | // A short string describing the purpose of the notification. Apple Watch 7 | // displays this string as part of the notification interface. This 8 | // string is displayed only briefly and should be crafted so that 9 | // it can be understood quickly. This key was added in iOS 8.2. 10 | Title string `json:"title,omitempty"` 11 | 12 | // The text of the alert message. 13 | Body string `json:"body,omitempty"` 14 | 15 | // The key to a title string in the Localizable.strings file for the current localization. 16 | // The key string can be formatted with %@ and %n$@ specifiers to take the variables 17 | // specified in the title-loc-args array. See Localized Formatted Strings for 18 | // more information.This key was added in iOS 8.2. 19 | TitleLocKey string `json:"title-loc-key,omitempty"` 20 | 21 | // Variable string values to appear in place of the format specifiers in title-loc-key. 22 | // See Localized Formatted Strings for more information.This key was added in iOS 8.2. 23 | TitleLocArgs []string `json:"title-loc-args,omitempty"` 24 | 25 | // If a string is specified, the system displays an alert that includes the Close and View buttons. 26 | // The string is used as a key to get a localized string in the current localization to use 27 | // for the right button’s title instead of “View”. See Localized Formatted Strings 28 | // for more information. 29 | ActionLocKey string `json:"action-loc-key,omitempty"` 30 | 31 | // A key to an alert-message string in a Localizable.strings file for the current 32 | // localization (which is set by the user’s language preference).The key string 33 | // can be formatted with %@ and %n$@ specifiers to take the variables 34 | // specified in the loc-args array. See Localized Formatted 35 | // Strings for more information. 36 | LocKey string `json:"loc-key,omitempty"` 37 | 38 | // Variable string values to appear in place of the format specifiers in loc-key. See Localized Formatted Strings for more information. 39 | LocArgs []string `json:"loc-args,omitempty"` 40 | 41 | // The filename of an image file in the app bundle; it may include the extension or omit it. 42 | // The image is used as the launch image when users tap the action button or move the 43 | // action slider.If this property is not specified, the system either uses the 44 | // previous snapshot,uses the image identified by the UILaunchImageFile key 45 | // in the app’s Info.plist file, or falls back to Default.png. 46 | // This property was added in iOS 4.0. 47 | LaunchImage string `json:"launch-image,omitempty"` 48 | } 49 | 50 | // “The following payload has an aps dictionary with a simple, recommended form 51 | // for alert messages with the default alert buttons (Close and View). 52 | // It uses a string as the value of alert rather than a 53 | // dictionary. This payload also has a custom array property. 54 | /* 55 | { 56 | "aps" : { "alert" : "Message received from Bob" }, 57 | "acme2" : [ "bang", "whiz" ] 58 | } 59 | */ 60 | func (a *Alert) isSimpleForm() bool { 61 | return len(a.Title) == 0 && len(a.TitleLocKey) == 0 && len(a.TitleLocArgs) == 0 && len(a.ActionLocKey) == 0 && len(a.LocKey) == 0 && len(a.LocArgs) == 0 && len(a.LaunchImage) == 0 62 | } 63 | 64 | // Returns bool if alert is valid 65 | func (a *Alert) isValid() bool { 66 | return a.isSimpleForm() && len(a.Body) == 0 67 | } 68 | -------------------------------------------------------------------------------- /alert_test.go: -------------------------------------------------------------------------------- 1 | package apns2_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/sger/go-apns2" 7 | ) 8 | 9 | func TestAlert(t *testing.T) { 10 | 11 | payload := apns2.Payload{ 12 | Alert: apns2.Alert{Body: "Hello World"}, 13 | } 14 | 15 | alert := apns2.Alert{ 16 | Body: "Hello World", 17 | } 18 | 19 | if alert.Body != payload.Alert.Body { 20 | t.Errorf("Expected %s, got %s", alert.Body, payload.Alert.Body) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apns_response.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | // ApnsResponse contains apns-id, reason, status code, status code description. 4 | type ApnsResponse struct { 5 | StatusCode int 6 | StatusCodeDescription string 7 | ApnsID string `json:"apns-id,omitempty"` 8 | Reason string `json:"reason,omitempty"` 9 | } 10 | -------------------------------------------------------------------------------- /certificate/certificate.go: -------------------------------------------------------------------------------- 1 | package certificate 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | 12 | "golang.org/x/crypto/pkcs12" 13 | ) 14 | 15 | // BlockType PEM formatted block (certificate, private key etc) 16 | type BlockType string 17 | 18 | func (bt BlockType) String() string { 19 | return string(bt) 20 | } 21 | 22 | // Type of BlockType 23 | const ( 24 | PrivateKey BlockType = "PRIVATE KEY" 25 | PublicKey BlockType = "PUBLIC KEY" 26 | RSAPrivateKey BlockType = "RSA PRIVATE KEY" 27 | ECPrivateKey BlockType = "EC PRIVATE KEY" 28 | Certificate BlockType = "CERTIFICATE" 29 | ) 30 | 31 | // ReadP12File reading a .p12 file 32 | func ReadP12File(filename string, password string) (tls.Certificate, error) { 33 | file, err := ioutil.ReadFile(filename) 34 | if err != nil { 35 | return tls.Certificate{}, fmt.Errorf("Error while loading %s: %v", filename, err) 36 | } 37 | 38 | // Decode the certification 39 | privateKey, cert, err := pkcs12.Decode(file, password) 40 | if err != nil { 41 | return tls.Certificate{}, err 42 | } 43 | 44 | // Verify the certification 45 | _, err = cert.Verify(x509.VerifyOptions{}) 46 | if err == nil { 47 | return tls.Certificate{}, err 48 | } 49 | 50 | switch e := err.(type) { 51 | case x509.CertificateInvalidError: 52 | switch e.Reason { 53 | case x509.Expired: 54 | // TODO Better support for error 55 | default: 56 | } 57 | case x509.UnknownAuthorityError: 58 | // TODO Better support for error 59 | default: 60 | } 61 | 62 | // check if private key is correct 63 | priv, b := privateKey.(*rsa.PrivateKey) 64 | if !b { 65 | return tls.Certificate{}, fmt.Errorf("Error with private key") 66 | } 67 | 68 | certificate := tls.Certificate{ 69 | Certificate: [][]byte{cert.Raw}, 70 | PrivateKey: priv, 71 | Leaf: cert, 72 | } 73 | 74 | //return cert, priv, nil 75 | return certificate, nil 76 | } 77 | 78 | // ReadPemFile parse .pem file returns tls.Certificate, error 79 | func ReadPemFile(filename string, password string) (tls.Certificate, error) { 80 | 81 | var certification tls.Certificate 82 | var block *pem.Block 83 | 84 | bytes, err := ioutil.ReadFile(filename) 85 | if err != nil { 86 | return tls.Certificate{}, err 87 | } 88 | 89 | if len(bytes) > 0 { 90 | for { 91 | block, bytes = pem.Decode(bytes) 92 | if block == nil { 93 | break 94 | } 95 | switch BlockType(block.Type) { 96 | case PrivateKey: 97 | // PrivateKey 98 | case PublicKey: 99 | // PublicKey 100 | case Certificate: 101 | cert, err := x509.ParseCertificate(block.Bytes) 102 | if err != nil { 103 | return tls.Certificate{}, err 104 | } 105 | certification.Leaf = cert 106 | certification.Certificate = append(certification.Certificate, block.Bytes) 107 | case RSAPrivateKey: 108 | if x509.IsEncryptedPEMBlock(block) { 109 | bytes, err := x509.DecryptPEMBlock(block, []byte(password)) 110 | if err != nil { 111 | return tls.Certificate{}, errors.New("Failed to decrypt private key") 112 | } 113 | key, err := x509.ParsePKCS1PrivateKey(bytes) 114 | if err != nil { 115 | return tls.Certificate{}, errors.New("Failed to parse PKCS1 private key") 116 | } 117 | certification.PrivateKey = key 118 | } else { 119 | key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 120 | if err != nil { 121 | return tls.Certificate{}, errors.New("Failed to parse PKCS1 private key") 122 | } 123 | certification.PrivateKey = key 124 | } 125 | case ECPrivateKey: 126 | //ECPrivateKey 127 | default: 128 | return tls.Certificate{}, fmt.Errorf("Decode Pem file: encountered unknown block type %s", block.Type) 129 | } 130 | } 131 | } 132 | return certification, nil 133 | } 134 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | 11 | "golang.org/x/net/http2" 12 | ) 13 | 14 | // Client struct with HTTPClient, Certificate, Host as parameters. 15 | type Client struct { 16 | HTTPClient *http.Client 17 | Certificate tls.Certificate 18 | Host string 19 | } 20 | 21 | // NewClient constructor tls.Certificate parameter. 22 | func NewClient(certificate tls.Certificate, host string) (*Client, error) { 23 | config := &tls.Config{ 24 | Certificates: []tls.Certificate{certificate}, 25 | } 26 | 27 | config.BuildNameToCertificate() 28 | 29 | transport := &http.Transport{TLSClientConfig: config} 30 | 31 | if err := http2.ConfigureTransport(transport); err != nil { 32 | return nil, err 33 | } 34 | 35 | client := &Client{ 36 | HTTPClient: &http.Client{Transport: transport}, 37 | Certificate: certificate, 38 | Host: host, 39 | } 40 | 41 | return client, nil 42 | } 43 | 44 | // SendPush a push notification with payload ([]byte), device token, *Headers 45 | // returns ApnsResponse struct 46 | func (c *Client) SendPush(payload interface{}, deviceToken string, headers *Headers) (*ApnsResponse, error) { 47 | 48 | b, err := json.Marshal(payload) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | url := fmt.Sprintf("%v/3/device/%v", c.Host, deviceToken) 54 | 55 | req, err := http.NewRequest("POST", url, bytes.NewReader(b)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // Send headers to request 61 | headers.Set(req.Header) 62 | 63 | resp, err := c.HTTPClient.Do(req) 64 | 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | apnsResponse := ApnsResponse{} 70 | apnsResponse.StatusCode = resp.StatusCode 71 | apnsResponse.StatusCodeDescription = statusCode[resp.StatusCode] 72 | 73 | if resp.StatusCode == http.StatusOK { 74 | apnsResponse.ApnsID = resp.Header.Get("apns-id") 75 | } 76 | 77 | defer resp.Body.Close() 78 | 79 | body, err := ioutil.ReadAll(resp.Body) 80 | 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | var errorResponse ErrorResponse 86 | json.Unmarshal(body, &errorResponse) 87 | 88 | if errorResponse.Reason != "" { 89 | apnsResponse.Reason = errorReason[errorResponse.Reason] 90 | } 91 | 92 | return &apnsResponse, nil 93 | } 94 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package apns2_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "reflect" 10 | "testing" 11 | 12 | "github.com/sger/go-apns2" 13 | ) 14 | 15 | func TestPush(t *testing.T) { 16 | deviceToken := "c7800a79efffe8ffc01b280717a936937cb69f8ca307545eb6983c60f12e167a" 17 | payload := apns2.Payload{ 18 | Alert: apns2.Alert{ 19 | Body: "Hello World"}, 20 | } 21 | 22 | apnsID := "674EB1D5-7E7C-3DC9-B0F5-32A55E54960E" 23 | 24 | handler := http.NewServeMux() 25 | server := httptest.NewServer(handler) 26 | 27 | handler.HandleFunc("/3/device/", func(w http.ResponseWriter, r *http.Request) { 28 | expectURL := fmt.Sprintf("/3/device/%s", deviceToken) 29 | if r.URL.String() != expectURL { 30 | t.Errorf("Expected url %v, got %v", expectURL, r.URL) 31 | } 32 | 33 | body, err := ioutil.ReadAll(r.Body) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | b, err := json.Marshal(payload) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if !reflect.DeepEqual(body, b) { 44 | t.Errorf("Expected body %v, got %v", payload, body) 45 | } 46 | w.Header().Set("apns-id", apnsID) 47 | }) 48 | 49 | client := apns2.Client{ 50 | HTTPClient: http.DefaultClient, 51 | Host: server.URL, 52 | } 53 | 54 | resp, err := client.SendPush(payload, deviceToken, &apns2.Headers{}) 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | 59 | remoteApnsID := resp.ApnsID 60 | 61 | if remoteApnsID != apnsID { 62 | t.Errorf("Expected apns-id %q, but got %q ", apnsID, remoteApnsID) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | // Apple Development and Production URLs 4 | const ( 5 | Development = "https://api.development.push.apple.com" 6 | Production = "https://api.push.apple.com" 7 | ) 8 | 9 | // Request headers 10 | const ( 11 | ApnsID = "apns-id" 12 | ApnsExpiration = "apns-expiration" 13 | ApnsPriority = "apns-priority" 14 | ApnsTopic = "apns-topic" 15 | ) 16 | -------------------------------------------------------------------------------- /error_codes.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | var errorReason = map[string]string{ 4 | "PayloadEmpty": "The message payload was empty.", 5 | "PayloadTooLarge": "The message payload was too large. The maximum payload size is 4096 bytes.", 6 | "BadTopic": "The apns-topic was invalid.", 7 | "TopicDisallowed": "Pushing to this topic is not allowed.", 8 | "BadMessageId": "The apns-id value is bad.", 9 | "BadExpirationDate": "The apns-expiration value is bad.", 10 | "BadPriority": "The apns-priority value is bad.", 11 | "MissingDeviceToken": "The device token is not specified in the request :path. Verify that the :path header contains the device token.", 12 | "BadDeviceToken": "The specified device token was bad. Verify that the request contains a valid token and that the token matches the environment.", 13 | "DeviceTokenNotForTopic": "The device token does not match the specified topic.", 14 | "Unregistered": "The device token is inactive for the specified topic.", 15 | "DuplicateHeaders": "One or more headers were repeated.", 16 | "BadCertificateEnvironment": "The client certificate was for the wrong environment.", 17 | "BadCertificate": "The certificate was bad.", 18 | "Forbidden": "The specified action is not allowed.", 19 | "BadPath": "The request contained a bad :path value.", 20 | "MethodNotAllowed": "The specified :method was not POST.", 21 | "TooManyRequests": "Too many requests were made consecutively to the same device token.", 22 | "IdleTimeout": "Idle time out.", 23 | "Shutdown": "The server is shutting down.", 24 | "InternalServerError": "An internal server error occurred.", 25 | "ServiceUnavailable": "The service is unavailable.", 26 | "MissingTopic": "The apns-topic header of the request was not specified and was required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics.", 27 | } 28 | -------------------------------------------------------------------------------- /error_response.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | // ErrorResponse contains reason, timestamp 4 | type ErrorResponse struct { 5 | Reason string `json:"reason,omitempty"` 6 | Timestamp int64 `json:"timestamp,omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /headers.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // Headers Request headers for apple push notification 10 | type Headers struct { 11 | ID string 12 | Expiration time.Time 13 | LowPriority bool 14 | Topic string 15 | } 16 | 17 | // Set request headers for HTTPClient 18 | func (h *Headers) Set(header http.Header) { 19 | 20 | header.Set("Content-Type", "application/json") 21 | 22 | if h.ID != "" { 23 | header.Set(ApnsID, h.ID) 24 | } 25 | 26 | if !h.Expiration.IsZero() { 27 | timestamp := strconv.FormatInt(h.Expiration.Unix(), 10) 28 | header.Set(ApnsExpiration, timestamp) 29 | } 30 | 31 | if h.LowPriority { 32 | header.Set(ApnsPriority, "5") 33 | } 34 | 35 | if h.Topic != "" { 36 | header.Set(ApnsTopic, h.Topic) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /headers_test.go: -------------------------------------------------------------------------------- 1 | package apns2_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/sger/go-apns2" 9 | ) 10 | 11 | func TestHeaders(t *testing.T) { 12 | headers := apns2.Headers{ 13 | ID: "12345", 14 | Expiration: time.Unix(1458565017, 0), 15 | LowPriority: true, 16 | Topic: "com.test.app-id", 17 | } 18 | 19 | requestHeader := http.Header{} 20 | headers.Set(requestHeader) 21 | 22 | testRequestHeader(t, requestHeader, "apns-id", headers.ID) 23 | testRequestHeader(t, requestHeader, "apns-expiration", "1458565017") 24 | testRequestHeader(t, requestHeader, "apns-priority", "5") 25 | testRequestHeader(t, requestHeader, "apns-topic", headers.Topic) 26 | } 27 | 28 | func testRequestHeader(t *testing.T, requestHeader http.Header, key string, expected string) { 29 | headerKey := requestHeader.Get(key) 30 | if headerKey != expected { 31 | t.Errorf("Expected %s %q, got %q.", key, expected, headerKey) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /payload.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | import "encoding/json" 4 | 5 | // Payload For each notification, compose a JSON dictionary object (as defined by RFC 4627). 6 | // This dictionary must contain another dictionary identified by the aps key. The aps 7 | // dictionary can contain one or more properties that specify the following user 8 | // notification types: An alert message to display to the user A number 9 | // to badge the app icon with A sound to play. 10 | type Payload struct { 11 | // If this property is included, the system displays a standard alert or a banner, based on the user’s setting. 12 | Alert Alert 13 | 14 | // The number to display as the badge of the app icon. If this 15 | // property is absent, the badge is not changed. To remove 16 | // the badge, set the value of this property to 0. 17 | Badge uint 18 | 19 | // The name of a sound file in the app bundle or in the Library/Sounds folder of the app’s data container. The sound 20 | // in this file is played as an alert. If the sound file doesn’t exist or default is specified as the value, 21 | // the default alert sound is played.The audio must be in one of the audio data formats that are 22 | // compatible with system sounds. 23 | Sound string 24 | 25 | // Provide this key with a value of 1 to indicate that new content is available. Including 26 | // this key and value means that when your app is launched in the background or resumed, 27 | // application:didReceiveRemoteNotification:fetchCompletionHandler: is called. 28 | ContentAvailable bool 29 | 30 | // Provide this key with a string value that represents the identifier 31 | // property of the UIMutableUserNotificationCategory object 32 | // you created to define custom actions 33 | Category string 34 | } 35 | 36 | // Map returns a valid payload 37 | func (p *Payload) Map() map[string]interface{} { 38 | payload := make(map[string]interface{}, 4) 39 | 40 | if !p.Alert.isValid() { 41 | if p.Alert.isSimpleForm() { 42 | payload["alert"] = p.Alert.Body 43 | } else { 44 | payload["alert"] = p.Alert 45 | } 46 | } 47 | 48 | if p.Badge != 0 { 49 | payload["badge"] = p.Badge 50 | } 51 | 52 | if p.Sound != "" { 53 | payload["sound"] = p.Sound 54 | } 55 | 56 | if p.ContentAvailable { 57 | payload["content-available"] = 1 58 | } 59 | 60 | if p.Category != "" { 61 | payload["category"] = p.Category 62 | } 63 | 64 | return map[string]interface{}{"aps": payload} 65 | } 66 | 67 | // MarshalJSON returns []byte, error 68 | func (p Payload) MarshalJSON() ([]byte, error) { 69 | return json.Marshal(p.Map()) 70 | } 71 | -------------------------------------------------------------------------------- /payload_test.go: -------------------------------------------------------------------------------- 1 | package apns2_test 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/sger/go-apns2" 9 | ) 10 | 11 | func TestPayload(t *testing.T) { 12 | var tests = []struct { 13 | input apns2.Payload 14 | expected []byte 15 | }{ 16 | { 17 | apns2.Payload{ 18 | Alert: apns2.Alert{Body: "Hello World"}, 19 | }, 20 | []byte(`{"aps":{"alert":"Hello World"}}`), 21 | }, 22 | { 23 | apns2.Payload{ 24 | Alert: apns2.Alert{ 25 | Title: "My Title", 26 | Body: "Hello APNS 2"}, 27 | }, 28 | []byte(`{"aps":{"alert":{"title":"My Title","body":"Hello APNS 2"}}}`), 29 | }, 30 | { 31 | apns2.Payload{ 32 | Alert: apns2.Alert{ 33 | Title: "My Title", 34 | Body: "Hello APNS 2", 35 | LocKey: "GAME_PLAY_REQUEST_FORMAT", 36 | LocArgs: []string{"Jenna", "Frank"}, 37 | }, 38 | }, 39 | []byte(`{"aps":{"alert":{"title":"My Title","body":"Hello APNS 2","loc-key":"GAME_PLAY_REQUEST_FORMAT","loc-args":["Jenna","Frank"]}}}`), 40 | }, 41 | { 42 | apns2.Payload{ 43 | Alert: apns2.Alert{ 44 | Title: "My Title", 45 | Body: "Hello APNS 2", 46 | LocKey: "GAME_PLAY_REQUEST_FORMAT", 47 | LocArgs: []string{"Jenna", "Frank"}, 48 | }, 49 | Badge: 2, 50 | }, 51 | []byte(`{"aps":{"alert":{"title":"My Title","body":"Hello APNS 2","loc-key":"GAME_PLAY_REQUEST_FORMAT","loc-args":["Jenna","Frank"]},"badge":2}}`), 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | testPayload(t, tt.input, tt.expected) 57 | } 58 | } 59 | 60 | func testPayload(t *testing.T, p interface{}, expected []byte) { 61 | 62 | payloadSize := 256 63 | 64 | b, err := json.Marshal(p) 65 | if err != nil { 66 | t.Fatal("Error", err) 67 | } 68 | 69 | if len(b) > payloadSize { 70 | t.Errorf("Expected payload to be less than %v instead sent %v", payloadSize, len(b)) 71 | } 72 | 73 | if !reflect.DeepEqual(b, expected) { 74 | t.Errorf("Expected %s, got %s", expected, b) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /status_codes.go: -------------------------------------------------------------------------------- 1 | package apns2 2 | 3 | var statusCode = map[int]string{ 4 | 200: "Success", 5 | 400: "Bad request", 6 | 403: "There was an error with the certificate.", 7 | 405: "The request used a bad :method value. Only POST requests are supported.", 8 | 410: "The device token is no longer active for the topic.", 9 | 413: "The notification payload was too large.", 10 | 429: "The server received too many requests for the same device token.", 11 | 500: "Internal server error", 12 | 503: "The server is shutting down and unavailable.", 13 | } 14 | --------------------------------------------------------------------------------