nexo/services/push-proxy/server/apple_notification_server.go

349 lines
9.6 KiB
Go

// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package server
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/url"
"time"
"github.com/kyokomi/emoji"
apns "github.com/sideshow/apns2"
"github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/payload"
"github.com/sideshow/apns2/token"
"golang.org/x/net/http2"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
type AppleNotificationServer struct {
AppleClient *apns.Client
metrics *metrics
logger *mlog.Logger
ApplePushSettings ApplePushSettings
sendTimeout time.Duration
retryTimeout time.Duration
}
func NewAppleNotificationServer(settings ApplePushSettings, logger *mlog.Logger, metrics *metrics, sendTimeoutSecs int, retryTimeoutSecs int) *AppleNotificationServer {
return &AppleNotificationServer{
ApplePushSettings: settings,
metrics: metrics,
logger: logger,
sendTimeout: time.Duration(sendTimeoutSecs) * time.Second,
retryTimeout: time.Duration(retryTimeoutSecs) * time.Second,
}
}
func (me *AppleNotificationServer) setupProxySettings(appleCert *tls.Certificate) error {
// Override the native transport.
proxyServer := getProxyServer()
if proxyServer != "" {
transport := &http.Transport{
Proxy: func(request *http.Request) (*url.URL, error) {
return url.Parse(proxyServer)
},
IdleConnTimeout: apns.HTTPClientTimeout,
}
if appleCert != nil {
transport.TLSClientConfig = &tls.Config{
Certificates: []tls.Certificate{*appleCert},
}
}
err := http2.ConfigureTransport(transport)
if err != nil {
return fmt.Errorf("Transport Error: %v", err)
}
me.AppleClient.HTTPClient.Transport = transport
}
if appleCert != nil {
me.logger.Info("Initializing apple notification server with PEM certificate", mlog.String("type", me.ApplePushSettings.Type))
} else {
me.logger.Info("Initializing apple notification server with AuthKey", mlog.String("type", me.ApplePushSettings.Type))
}
return nil
}
func (me *AppleNotificationServer) Initialize() error {
if me.ApplePushSettings.AppleAuthKeyFile != "" && me.ApplePushSettings.AppleAuthKeyID != "" && me.ApplePushSettings.AppleTeamID != "" {
authKey, err := token.AuthKeyFromFile(me.ApplePushSettings.AppleAuthKeyFile)
if err != nil {
return fmt.Errorf("Failed to initialize apple notification service with AuthKey file err=%v ", err)
}
appleToken := &token.Token{
AuthKey: authKey,
KeyID: me.ApplePushSettings.AppleAuthKeyID,
TeamID: me.ApplePushSettings.AppleTeamID,
}
if me.ApplePushSettings.ApplePushUseDevelopment {
me.AppleClient = apns.NewTokenClient(appleToken).Development()
} else {
me.AppleClient = apns.NewTokenClient(appleToken).Production()
}
// Override the native transport.
return me.setupProxySettings(nil)
}
if me.ApplePushSettings.ApplePushCertPrivate != "" {
appleCert, appleCertErr := certificate.FromPemFile(me.ApplePushSettings.ApplePushCertPrivate, me.ApplePushSettings.ApplePushCertPassword)
if appleCertErr != nil {
return fmt.Errorf("Failed to initialize apple notification service with pem cert err=%v for type=%v", appleCertErr, me.ApplePushSettings.Type)
}
if me.ApplePushSettings.ApplePushUseDevelopment {
me.AppleClient = apns.NewClient(appleCert).Development()
} else {
me.AppleClient = apns.NewClient(appleCert).Production()
}
// Override the native transport.
return me.setupProxySettings(&appleCert)
}
return fmt.Errorf("Apple push notifications not configured. Missing ApplePushCertPrivate. for type=%v", me.ApplePushSettings.Type)
}
func (me *AppleNotificationServer) SendNotification(msg *PushNotification) PushResponse {
data := payload.NewPayload()
if msg.Badge == 0 && msg.Type == PushTypeClear && msg.AppVersion > 1 {
data.Badge(1)
} else if msg.Badge != -1 {
data.Badge(msg.Badge)
}
notification := &apns.Notification{}
notification.DeviceToken = msg.DeviceID
notification.Payload = data
notification.Topic = me.ApplePushSettings.ApplePushTopic
notification.Priority = apns.PriorityHigh
var pushType = msg.Type
if msg.IsIDLoaded {
data.Category(msg.Category)
data.Sound("default")
data.Custom("version", msg.Version)
data.Custom("id_loaded", true)
data.MutableContent()
data.AlertBody(msg.Message)
data.ContentAvailable()
} else {
switch msg.Type {
case PushTypeMessage, PushTypeSession:
data.Category(msg.Category)
data.Sound("default")
data.Custom("version", msg.Version)
data.MutableContent()
if msg.Type == PushTypeMessage {
data.ContentAvailable()
}
if msg.ChannelName != "" && msg.Version == "v2" {
data.AlertTitle(msg.ChannelName)
data.AlertBody(emoji.Sprint(msg.Message))
data.Custom("channel_name", msg.ChannelName)
} else {
data.Alert(emoji.Sprint(msg.Message))
if msg.ChannelName != "" {
data.Custom("channel_name", msg.ChannelName)
}
}
case PushTypeClear, PushTypeTest:
data.ContentAvailable()
case PushTypeUpdateBadge:
// Handled by the apps, nothing else to do here
}
}
if me.metrics != nil {
me.metrics.incrementNotificationTotal(PushNotifyApple, pushType)
}
data.Custom("type", pushType)
data.Custom("sub_type", msg.SubType)
data.Custom("server_id", msg.ServerID)
if msg.AckID != "" {
data.Custom("ack_id", msg.AckID)
}
data.Custom("is_crt_enabled", msg.IsCRTEnabled)
if msg.ChannelID != "" {
data.Custom("channel_id", msg.ChannelID)
if msg.IsCRTEnabled && msg.RootID != "" {
data.ThreadID(msg.RootID)
} else {
data.ThreadID(msg.ChannelID)
}
}
if msg.Signature == "" {
data.Custom("signature", "NO_SIGNATURE")
} else {
data.Custom("signature", msg.Signature)
}
if msg.TeamID != "" {
data.Custom("team_id", msg.TeamID)
}
if msg.SenderID != "" {
data.Custom("sender_id", msg.SenderID)
}
if msg.SenderName != "" {
data.Custom("sender_name", msg.SenderName)
}
if msg.PostID != "" {
data.Custom("post_id", msg.PostID)
}
if msg.RootID != "" {
data.Custom("root_id", msg.RootID)
}
if msg.OverrideUsername != "" {
data.Custom("override_username", msg.OverrideUsername)
}
if msg.OverrideIconURL != "" {
data.Custom("override_icon_url", msg.OverrideIconURL)
}
if msg.FromWebhook != "" {
data.Custom("from_webhook", msg.FromWebhook)
}
if me.AppleClient != nil {
me.logger.Info(
"Sending apple push notification",
mlog.String("device", me.ApplePushSettings.Type),
mlog.String("type", msg.Type),
mlog.String("ack_id", msg.AckID),
)
res, err := me.SendNotificationWithRetry(notification)
if err != nil {
me.logger.Error(
"Failed to send apple push",
mlog.String("sid", msg.ServerID),
mlog.String("did", msg.DeviceID),
mlog.Err(err),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyApple, pushType, "RequestError")
}
return NewErrorPushResponse("unknown transport error")
}
if !res.Sent() {
if res.Reason == apns.ReasonBadDeviceToken || res.Reason == apns.ReasonUnregistered || res.Reason == apns.ReasonMissingDeviceToken || res.Reason == apns.ReasonDeviceTokenNotForTopic {
me.logger.Info(
"Failed to send apple push sending remove code res",
mlog.String("ApnsID", res.ApnsID),
mlog.String("reason", res.Reason),
mlog.Int("code", res.StatusCode),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementRemoval(PushNotifyApple, pushType, res.Reason)
}
return NewRemovePushResponse()
}
me.logger.Error(
"Failed to send apple push with res",
mlog.String("ApnsID", res.ApnsID),
mlog.String("reason", res.Reason),
mlog.Int("code", res.StatusCode),
mlog.String("type", me.ApplePushSettings.Type),
)
if me.metrics != nil {
me.metrics.incrementFailure(PushNotifyApple, pushType, res.Reason)
}
return NewErrorPushResponse("unknown send response error")
}
}
if me.metrics != nil {
if msg.AckID != "" {
me.metrics.incrementSuccessWithAck(PushNotifyApple, pushType)
} else {
me.metrics.incrementSuccess(PushNotifyApple, pushType)
}
}
return NewOkPushResponse()
}
func (me *AppleNotificationServer) SendNotificationWithRetry(notification *apns.Notification) (*apns.Response, error) {
var res *apns.Response
var err error
waitTime := time.Second
// Keep a general context to make sure the whole retry
// doesn't take longer than the timeout.
generalContext, cancelGeneralContext := context.WithTimeout(context.Background(), me.sendTimeout)
defer cancelGeneralContext()
for retries := 0; retries < MAX_RETRIES; retries++ {
start := time.Now()
retryContext, cancelRetryContext := context.WithTimeout(generalContext, me.retryTimeout)
defer cancelRetryContext()
res, err = me.AppleClient.PushWithContext(retryContext, notification)
if me.metrics != nil {
me.metrics.observerNotificationResponse(PushNotifyApple, time.Since(start).Seconds())
}
if err == nil {
break
}
me.logger.Error(
"Failed to send apple push",
mlog.String("did", notification.DeviceToken),
mlog.Int("retry", retries),
mlog.Err(err),
)
if retries == MAX_RETRIES-1 {
me.logger.Error("Max retries reached", mlog.String("did", notification.DeviceToken))
break
}
select {
case <-generalContext.Done():
case <-time.After(waitTime):
}
if generalContext.Err() != nil {
me.logger.Info(
"Not retrying because context error",
mlog.String("did", notification.DeviceToken),
mlog.Int("retry", retries),
mlog.Err(generalContext.Err()),
)
err = generalContext.Err()
break
}
waitTime *= 2
}
return res, err
}