Commit 5a239967 authored by Nikita Chernyi's avatar Nikita Chernyi

Added APNS support

parent f33406e2
Pipeline #69029635 passed with stage
in 1 minute and 39 seconds
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
digest = "1:358e40993cbf507335b7a9adfed14c1d2caadeca71a5ba65fa0c3d0d55b41cc4"
name = "github.com/Coccodrillo/apns"
packages = ["."]
pruneopts = "UT"
revision = "91763352f7bfc26cca140cda1dad70e0ec9a4236"
[[projects]]
branch = "master"
digest = "1:2fd9fb94f0bf18032fe47bc12ed6f83ba08053fad7f2a88c4ae927c051e85177"
......@@ -9,6 +17,14 @@
pruneopts = "UT"
revision = "808e978ddcd20641adbc07563258edcf3513c7ed"
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:c950e574951c7199fb3d990d0e7a61996f40f8e646ba7cf8a557878d4c737f53"
name = "github.com/go-redis/redis"
......@@ -25,10 +41,38 @@
revision = "75795aa4236dc7341eefac3bbe945e68c99ef9df"
version = "v6.15.3"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:ac83cf90d08b63ad5f7e020ef480d319ae890c208f8524622a2f3136e2686b02"
name = "github.com/stretchr/objx"
packages = ["."]
pruneopts = "UT"
revision = "477a77ecc69700c7cdeb1fa9e129548e1c1c393c"
version = "v0.1.1"
[[projects]]
digest = "1:0bcc464dabcfad5393daf87c3f8142911d0f6c52569b837e91a1c15e890265f3"
name = "github.com/stretchr/testify"
packages = [
"assert",
"mock",
]
pruneopts = "UT"
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.3.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/Coccodrillo/apns",
"github.com/NaySoftware/go-fcm",
"github.com/go-redis/redis",
]
......
......@@ -7,10 +7,26 @@ Example request:
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"test", "token":"eIu0Ds5E8l0:APA91bEKvZJRuaiZZMmiRArOGx454yePPHaoLQtqqBI3tiXNA_Evu7uBFqMXbn7xZBfR2r4SDQEOj514vmd4L3Nxe_U_Sw9MdOfe1cOkU-sfn2QPlKzSaEzAinihlzqSCai9_n_lS2oE","body":"newbody", "priority": "high", "sound": "sound.mp3", "transport": "fcm"}' \
-d '{"title":"test", "token":"lJo3Kr4X9s1:APA91bEKvZJRuaiZZMmiRArOGx454yePPHaoLQtqqBI3tiXNA_Evu7uBFqMXbn7xZBfR2r4SDQEOj514vmd4L3Nxe_U_Sw9MdOfe1cOkU-sfn2QPlKzSaEzAinihlzqSCai9_n_lS2oE","body":"newbody", "priority": "high", "sound": "sound.mp3", "transport": "fcm"}' \
http://localhost
```
## Features
### Listens on
* Redis
* HTTP
on the same time. If one of listeners is broken, the second one will work.
### Sends to
* FCM
* APNS
on the same time
## Configuration
This health check needs a `config.json` file with this minimal structure
......
......@@ -13,6 +13,8 @@
"token": "FCM Server Token (formely server key)"
},
"apns": {
"cert": "/path/to/apns.pem"
},
"gateway": "gateway.sandbox.push.apple.com:2195",
"cert": "/path/to/cert.pem",
"key": "/path/to/key-noenc.pem"
}
}
......@@ -22,6 +22,11 @@ type Config struct {
Fcm struct {
Token string
}
Apns struct {
Gateway string
Cert string
Key string
}
}
// CreateConfigurationFromFile Returns a new configuration loaded from a file
......
......@@ -23,7 +23,9 @@ func handleHttp(w http.ResponseWriter, r *http.Request) {
if err != nil {
log.Println("Error parsing HTTP request: ", err)
}
log.Println("HTTP Request", string(body))
message := sender.Parse(body)
go sender.Send(message)
if len(body) > 0 {
log.Println("HTTP Request", string(body))
message := sender.Parse(body)
go sender.Send(message)
}
}
......@@ -16,6 +16,6 @@ func main() {
if err != nil {
log.Fatalf("Could not load file: %v", err)
}
sender.Config = config
sender.Cfg = config
listener.Listen(config)
}
package sender
import (
"log"
"github.com/Coccodrillo/apns"
)
func sendApns(gateway string, cert string, key string, message Message) error {
var err error
payload := apns.NewPayload()
payload.Alert = message.Title
payload.Sound = message.Sound
apnsMessage := apns.NewPushNotification()
apnsMessage.DeviceToken = message.Token
apnsMessage.AddPayload(payload)
apnsMessage.Set("data", message.Data)
apnsMessage.Set("body", message.Body)
client := apns.NewClient(gateway, cert, key)
status := client.Send(apnsMessage)
log.Println("APNS notification sent:", status.Success, status.Error)
log.Println(apnsMessage.PayloadString())
if status.Error != nil {
err = status.Error
}
return err
}
......@@ -5,7 +5,7 @@ import (
"errors"
"log"
. "gitlab.com/rakshazi/push/config"
"gitlab.com/rakshazi/push/config"
)
type Message struct {
......@@ -21,7 +21,7 @@ type Message struct {
} `json:"data,omitempty"`
}
var Config Config
var Cfg config.Config
func Parse(jsonString []byte) Message {
message := Message{
......@@ -37,7 +37,10 @@ func Parse(jsonString []byte) Message {
func Send(message Message) error {
if message.Transport == "fcm" {
return sendFcm(Config.Fcm.Token, message)
return sendFcm(Cfg.Fcm.Token, message)
}
if message.Transport == "apns" {
return sendApns(Cfg.Apns.Gateway, Cfg.Apns.Cert, Cfg.Apns.Key, message)
}
return errors.New("Message uses wrong transport:" + message.Transport)
......
# 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
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
# 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: <nil>
```
### 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.
```
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
}
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
}
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 {