Support new Apple Token Based Authentication (JWT) (#300)

* Support new Apple Token Based Authentication (JWT)

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* fix testing

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
Bo-Yi Wu 2017-10-25 03:49:23 -05:00 committed by GitHub
parent 461a57ec9a
commit c06e819e08
9 changed files with 83 additions and 15 deletions

View File

@ -113,6 +113,8 @@ ios:
password: "" # certificate password, default as empty string. password: "" # certificate password, default as empty string.
production: false production: false
max_retry: 0 # resend fail notification, default value zero is disabled max_retry: 0 # resend fail notification, default value zero is disabled
key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
team_id: "" # TeamID from developer account (View Account -> Membership)
log: log:
format: "string" # string or json format: "string" # string or json

View File

@ -0,0 +1,3 @@
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfZZZ

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEbVzfPnZPxfAyxqE
ZV05laAoJAl+/6Xt2O4mOB611sOhRANCAASgFTKjwJAAU95g++/vzKWHkzAVmNMI
tB5vTjZOOIwnEb70MsWZFIyUFD1P9Gwstz4+akHX7vI8BH6hHmBmfeQl
-----END PRIVATE KEY-----

View File

@ -56,6 +56,8 @@ ios:
password: "" # certificate password, default as empty string. password: "" # certificate password, default as empty string.
production: false production: false
max_retry: 0 # resend fail notification, default value zero is disabled max_retry: 0 # resend fail notification, default value zero is disabled
key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
team_id: "" # TeamID from developer account (View Account -> Membership)
log: log:
format: "string" # string or json format: "string" # string or json
@ -140,6 +142,8 @@ type SectionIos struct {
Password string `yaml:"password"` Password string `yaml:"password"`
Production bool `yaml:"production"` Production bool `yaml:"production"`
MaxRetry int `yaml:"max_retry"` MaxRetry int `yaml:"max_retry"`
KeyID string `yaml:"key_id"`
TeamID string `yaml:"team_id"`
} }
// SectionLog is sub section of config. // SectionLog is sub section of config.
@ -269,6 +273,8 @@ func LoadConf(confPath string) (ConfYaml, error) {
conf.Ios.Password = viper.GetString("ios.password") conf.Ios.Password = viper.GetString("ios.password")
conf.Ios.Production = viper.GetBool("ios.production") conf.Ios.Production = viper.GetBool("ios.production")
conf.Ios.MaxRetry = viper.GetInt("ios.max_retry") conf.Ios.MaxRetry = viper.GetInt("ios.max_retry")
conf.Ios.KeyID = viper.GetString("ios.key_id")
conf.Ios.TeamID = viper.GetString("ios.team_id")
// log // log
conf.Log.Format = viper.GetString("log.format") conf.Log.Format = viper.GetString("log.format")

View File

@ -43,6 +43,8 @@ ios:
password: "" # certificate password, default as empty string. password: "" # certificate password, default as empty string.
production: false production: false
max_retry: 0 # resend fail notification, default value zero is disabled max_retry: 0 # resend fail notification, default value zero is disabled
key_id: "" # KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
team_id: "" # TeamID from developer account (View Account -> Membership)
log: log:
format: "string" # string or json format: "string" # string or json

View File

@ -76,6 +76,8 @@ func (suite *ConfigTestSuite) TestValidateConfDefault() {
assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.Password) assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.Password)
assert.Equal(suite.T(), false, suite.ConfGorushDefault.Ios.Production) assert.Equal(suite.T(), false, suite.ConfGorushDefault.Ios.Production)
assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Ios.MaxRetry) assert.Equal(suite.T(), 0, suite.ConfGorushDefault.Ios.MaxRetry)
assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.KeyID)
assert.Equal(suite.T(), "", suite.ConfGorushDefault.Ios.TeamID)
// log // log
assert.Equal(suite.T(), "string", suite.ConfGorushDefault.Log.Format) assert.Equal(suite.T(), "string", suite.ConfGorushDefault.Log.Format)
@ -141,6 +143,8 @@ func (suite *ConfigTestSuite) TestValidateConf() {
assert.Equal(suite.T(), "", suite.ConfGorush.Ios.Password) assert.Equal(suite.T(), "", suite.ConfGorush.Ios.Password)
assert.Equal(suite.T(), false, suite.ConfGorush.Ios.Production) assert.Equal(suite.T(), false, suite.ConfGorush.Ios.Production)
assert.Equal(suite.T(), 0, suite.ConfGorush.Ios.MaxRetry) assert.Equal(suite.T(), 0, suite.ConfGorush.Ios.MaxRetry)
assert.Equal(suite.T(), "", suite.ConfGorush.Ios.KeyID)
assert.Equal(suite.T(), "", suite.ConfGorush.Ios.TeamID)
// log // log
assert.Equal(suite.T(), "string", suite.ConfGorush.Log.Format) assert.Equal(suite.T(), "string", suite.ConfGorush.Log.Format)
@ -174,6 +178,8 @@ func TestLoadConfigFromEnv(t *testing.T) {
os.Setenv("GORUSH_CORE_PORT", "9001") os.Setenv("GORUSH_CORE_PORT", "9001")
os.Setenv("GORUSH_GRPC_ENABLED", "true") os.Setenv("GORUSH_GRPC_ENABLED", "true")
os.Setenv("GORUSH_CORE_MAX_NOTIFICATION", "200") os.Setenv("GORUSH_CORE_MAX_NOTIFICATION", "200")
os.Setenv("GORUSH_IOS_KEY_ID", "ABC123DEFG")
os.Setenv("GORUSH_IOS_TEAM_ID", "DEF123GHIJ")
ConfGorush, err := LoadConf("config.yml") ConfGorush, err := LoadConf("config.yml")
if err != nil { if err != nil {
panic("failed to load config.yml from file") panic("failed to load config.yml from file")
@ -181,4 +187,6 @@ func TestLoadConfigFromEnv(t *testing.T) {
assert.Equal(t, "9001", ConfGorush.Core.Port) assert.Equal(t, "9001", ConfGorush.Core.Port)
assert.Equal(t, int64(200), ConfGorush.Core.MaxNotification) assert.Equal(t, int64(200), ConfGorush.Core.MaxNotification)
assert.True(t, ConfGorush.GRPC.Enabled) assert.True(t, ConfGorush.GRPC.Enabled)
assert.Equal(t, "ABC123DEFG", ConfGorush.Ios.KeyID)
assert.Equal(t, "DEF123GHIJ", ConfGorush.Ios.TeamID)
} }

View File

@ -1,13 +1,11 @@
package gorush package gorush
import ( import (
"crypto/tls"
"github.com/appleboy/gorush/config" "github.com/appleboy/gorush/config"
"github.com/appleboy/gorush/storage" "github.com/appleboy/gorush/storage"
"github.com/appleboy/go-fcm" "github.com/appleboy/go-fcm"
apns "github.com/sideshow/apns2" "github.com/sideshow/apns2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -16,10 +14,8 @@ var (
PushConf config.ConfYaml PushConf config.ConfYaml
// QueueNotification is chan type // QueueNotification is chan type
QueueNotification chan PushNotification QueueNotification chan PushNotification
// CertificatePemIos is ios certificate file
CertificatePemIos tls.Certificate
// ApnsClient is apns client // ApnsClient is apns client
ApnsClient *apns.Client ApnsClient *apns2.Client
// FCMClient is apns client // FCMClient is apns client
FCMClient *fcm.Client FCMClient *fcm.Client
// LogAccess is log server request log // LogAccess is log server request log

View File

@ -1,26 +1,33 @@
package gorush package gorush
import ( import (
"crypto/ecdsa"
"crypto/tls"
"errors" "errors"
"path/filepath" "path/filepath"
"time" "time"
apns "github.com/sideshow/apns2" "github.com/sideshow/apns2"
"github.com/sideshow/apns2/certificate" "github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/payload" "github.com/sideshow/apns2/payload"
"github.com/sideshow/apns2/token"
) )
// InitAPNSClient use for initialize APNs Client. // InitAPNSClient use for initialize APNs Client.
func InitAPNSClient() error { func InitAPNSClient() error {
if PushConf.Ios.Enabled { if PushConf.Ios.Enabled {
var err error var err error
var authKey *ecdsa.PrivateKey
var certificateKey tls.Certificate
ext := filepath.Ext(PushConf.Ios.KeyPath) ext := filepath.Ext(PushConf.Ios.KeyPath)
switch ext { switch ext {
case ".p12": case ".p12":
CertificatePemIos, err = certificate.FromP12File(PushConf.Ios.KeyPath, PushConf.Ios.Password) certificateKey, err = certificate.FromP12File(PushConf.Ios.KeyPath, PushConf.Ios.Password)
case ".pem": case ".pem":
CertificatePemIos, err = certificate.FromPemFile(PushConf.Ios.KeyPath, PushConf.Ios.Password) certificateKey, err = certificate.FromPemFile(PushConf.Ios.KeyPath, PushConf.Ios.Password)
case ".p8":
authKey, err = token.AuthKeyFromFile(PushConf.Ios.KeyPath)
default: default:
err = errors.New("wrong certificate key extension") err = errors.New("wrong certificate key extension")
} }
@ -31,10 +38,25 @@ func InitAPNSClient() error {
return err return err
} }
if ext == ".p8" && PushConf.Ios.KeyID != "" && PushConf.Ios.TeamID != "" {
token := &token.Token{
AuthKey: authKey,
// KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
KeyID: PushConf.Ios.KeyID,
// TeamID from developer account (View Account -> Membership)
TeamID: PushConf.Ios.TeamID,
}
if PushConf.Ios.Production { if PushConf.Ios.Production {
ApnsClient = apns.NewClient(CertificatePemIos).Production() ApnsClient = apns2.NewTokenClient(token).Production()
} else { } else {
ApnsClient = apns.NewClient(CertificatePemIos).Development() ApnsClient = apns2.NewTokenClient(token).Development()
}
} else {
if PushConf.Ios.Production {
ApnsClient = apns2.NewClient(certificateKey).Production()
} else {
ApnsClient = apns2.NewClient(certificateKey).Development()
}
} }
} }
@ -101,8 +123,8 @@ func iosAlertDictionary(payload *payload.Payload, req PushNotification) *payload
// GetIOSNotification use for define iOS notification. // GetIOSNotification use for define iOS notification.
// The iOS Notification Payload // The iOS Notification Payload
// ref: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1 // ref: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1
func GetIOSNotification(req PushNotification) *apns.Notification { func GetIOSNotification(req PushNotification) *apns2.Notification {
notification := &apns.Notification{ notification := &apns2.Notification{
ApnsID: req.ApnsID, ApnsID: req.ApnsID,
Topic: req.Topic, Topic: req.Topic,
} }
@ -112,7 +134,7 @@ func GetIOSNotification(req PushNotification) *apns.Notification {
} }
if len(req.Priority) > 0 && req.Priority == "normal" { if len(req.Priority) > 0 && req.Priority == "normal" {
notification.Priority = apns.PriorityLow notification.Priority = apns2.PriorityLow
} }
payload := payload.NewPayload() payload := payload.NewPayload()

View File

@ -412,6 +412,30 @@ func TestAPNSClientProdHost(t *testing.T) {
assert.Equal(t, apns2.HostProduction, ApnsClient.Host) assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
} }
func TestAPNSClientInvaildToken(t *testing.T) {
PushConf, _ = config.LoadConf("")
PushConf.Ios.Enabled = true
PushConf.Ios.KeyPath = "../certificate/authkey-invalid.p8"
err := InitAPNSClient()
assert.Error(t, err)
}
func TestAPNSClientVaildToken(t *testing.T) {
PushConf, _ = config.LoadConf("")
PushConf.Ios.Enabled = true
PushConf.Ios.KeyPath = "../certificate/authkey-valid.p8"
err := InitAPNSClient()
assert.NoError(t, err)
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
PushConf.Ios.Production = true
err = InitAPNSClient()
assert.NoError(t, err)
assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
}
func TestPushToIOS(t *testing.T) { func TestPushToIOS(t *testing.T) {
PushConf, _ = config.LoadConf("") PushConf, _ = config.LoadConf("")