Support HTTP proxy for APNs (#445)

This commit is contained in:
Keisuke Emi 2019-12-08 08:30:24 +09:00 committed by Bo-Yi Wu
parent 6f2b614265
commit 1edfa9f532
6 changed files with 128 additions and 26 deletions

View File

@ -77,7 +77,7 @@ A push notification micro server using [Gin](https://github.com/gin-gonic/gin) f
- Support store app stat to memory, [Redis](http://redis.io/), [BoltDB](https://github.com/boltdb/bolt), [BuntDB](https://github.com/tidwall/buntdb), [LevelDB](https://github.com/syndtr/goleveldb) or [BadgerDB](https://github.com/dgraph-io/badger). - Support store app stat to memory, [Redis](http://redis.io/), [BoltDB](https://github.com/boltdb/bolt), [BuntDB](https://github.com/tidwall/buntdb), [LevelDB](https://github.com/syndtr/goleveldb) or [BadgerDB](https://github.com/dgraph-io/badger).
- Support `p8`, `p12` or `pem` format of iOS certificate file. - Support `p8`, `p12` or `pem` format of iOS certificate file.
- Support `/sys/stats` show response time, status code count, etc. - Support `/sys/stats` show response time, status code count, etc.
- Support for HTTP proxy to Google server (FCM). - Support for HTTP proxy.
- Support retry send notification if server response is fail. - Support retry send notification if server response is fail.
- Support expose [prometheus](https://prometheus.io/) metrics. - Support expose [prometheus](https://prometheus.io/) metrics.
- Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically. - Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically.
@ -103,7 +103,7 @@ core:
key_path: "key.pem" key_path: "key.pem"
cert_base64: "" cert_base64: ""
key_base64: "" key_base64: ""
http_proxy: "" # only working for FCM server http_proxy: ""
pid: pid:
enabled: false enabled: false
path: "gorush.pid" path: "gorush.pid"
@ -254,7 +254,7 @@ Server Options:
-t, --token <token> Notification token -t, --token <token> Notification token
-e, --engine <engine> Storage engine (memory, redis ...) -e, --engine <engine> Storage engine (memory, redis ...)
--title <title> Notification title --title <title> Notification title
--proxy <proxy> Proxy URL (only for GCM) --proxy <proxy> Proxy URL
--pid <pid path> Process identifier path --pid <pid path> Process identifier path
--redis-addr <redis addr> Redis addr (default: localhost:6379) --redis-addr <redis addr> Redis addr (default: localhost:6379)
iOS Options: iOS Options:
@ -292,7 +292,7 @@ gorush --android --topic "/topics/foo-bar" \
- `-t`: Device token. - `-t`: Device token.
- `--title`: Notification title. - `--title`: Notification title.
- `--topic`: Send messages to topics. note: don't add device token. - `--topic`: Send messages to topics. note: don't add device token.
- `--proxy`: Set http proxy url. (only working for FCM) - `--proxy`: Set http proxy url.
### Send iOS notification ### Send iOS notification

View File

@ -26,7 +26,7 @@ core:
key_path: "key.pem" key_path: "key.pem"
cert_base64: "" cert_base64: ""
key_base64: "" key_base64: ""
http_proxy: "" # only working for FCM server http_proxy: ""
pid: pid:
enabled: false enabled: false
path: "gorush.pid" path: "gorush.pid"

View File

@ -13,7 +13,7 @@ core:
key_path: "key.pem" key_path: "key.pem"
cert_base64: "" cert_base64: ""
key_base64: "" key_base64: ""
http_proxy: "" # only working for FCM server http_proxy: ""
pid: pid:
enabled: false enabled: false
path: "gorush.pid" path: "gorush.pid"

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors" "errors"
"net/http"
"path/filepath" "path/filepath"
"time" "time"
@ -14,8 +15,11 @@ import (
"github.com/sideshow/apns2/payload" "github.com/sideshow/apns2/payload"
"github.com/sideshow/apns2/token" "github.com/sideshow/apns2/token"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
) )
var idleConnTimeout = 90 * time.Second
// Sound sets the aps sound on the payload. // Sound sets the aps sound on the payload.
type Sound struct { type Sound struct {
Critical int `json:"critical,omitempty"` Critical int `json:"critical,omitempty"`
@ -84,23 +88,87 @@ func InitAPNSClient() error {
// TeamID from developer account (View Account -> Membership) // TeamID from developer account (View Account -> Membership)
TeamID: PushConf.Ios.TeamID, TeamID: PushConf.Ios.TeamID,
} }
if PushConf.Ios.Production {
ApnsClient = apns2.NewTokenClient(token).Production() ApnsClient, err = newApnsTokenClient(token)
} else { } else {
ApnsClient = apns2.NewTokenClient(token).Development() ApnsClient, err = newApnsClient(certificateKey)
}
} else {
if PushConf.Ios.Production {
ApnsClient = apns2.NewClient(certificateKey).Production()
} else {
ApnsClient = apns2.NewClient(certificateKey).Development()
} }
if err != nil {
LogError.Error("Transport Error:", err.Error())
return err
} }
} }
return nil return nil
} }
func newApnsClient(certificate tls.Certificate) (*apns2.Client, error) {
var client *apns2.Client
if PushConf.Ios.Production {
client = apns2.NewClient(certificate).Production()
} else {
client = apns2.NewClient(certificate).Development()
}
if PushConf.Core.HTTPProxy == "" {
return client, nil
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{certificate},
}
if len(certificate.Certificate) > 0 {
tlsConfig.BuildNameToCertificate()
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.DefaultTransport.(*http.Transport).Proxy,
IdleConnTimeout: idleConnTimeout,
}
transportErr := http2.ConfigureTransport(transport)
if transportErr != nil {
return nil, transportErr
}
client.HTTPClient.Transport = transport
return client, nil
}
func newApnsTokenClient(token *token.Token) (*apns2.Client, error) {
var client *apns2.Client
if PushConf.Ios.Production {
client = apns2.NewTokenClient(token).Production()
} else {
client = apns2.NewTokenClient(token).Development()
}
if PushConf.Core.HTTPProxy == "" {
return client, nil
}
transport := &http.Transport{
Proxy: http.DefaultTransport.(*http.Transport).Proxy,
IdleConnTimeout: idleConnTimeout,
}
transportErr := http2.ConfigureTransport(transport)
if transportErr != nil {
return nil, transportErr
}
client.HTTPClient.Transport = transport
return client, nil
}
func iosAlertDictionary(payload *payload.Payload, req PushNotification) *payload.Payload { func iosAlertDictionary(payload *payload.Payload, req PushNotification) *payload.Payload {
// Alert dictionary // Alert dictionary

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"log" "log"
"net/http"
"net/url"
"os" "os"
"testing" "testing"
"time" "time"
@ -638,6 +640,42 @@ func TestAPNSClientVaildToken(t *testing.T) {
assert.Equal(t, apns2.HostProduction, ApnsClient.Host) assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
} }
func TestAPNSClientUseProxy(t *testing.T) {
PushConf, _ = config.LoadConf("")
PushConf.Ios.Enabled = true
PushConf.Ios.KeyPath = "../certificate/certificate-valid.p12"
PushConf.Core.HTTPProxy = "http://127.0.0.1:8080"
_ = SetProxy(PushConf.Core.HTTPProxy)
err := InitAPNSClient()
assert.Nil(t, err)
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
req, _ := http.NewRequest("GET", apns2.HostDevelopment, nil)
actualProxyURL, err := ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req)
assert.Nil(t, err)
expectedProxyURL, _ := url.ParseRequestURI(PushConf.Core.HTTPProxy)
assert.Equal(t, expectedProxyURL, actualProxyURL)
PushConf.Ios.KeyPath = "../certificate/authkey-valid.p8"
PushConf.Ios.TeamID = "example.team"
PushConf.Ios.KeyID = "example.key"
err = InitAPNSClient()
assert.Nil(t, err)
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
assert.NotNil(t, ApnsClient.Token)
req, _ = http.NewRequest("GET", apns2.HostDevelopment, nil)
actualProxyURL, err = ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req)
assert.Nil(t, err)
expectedProxyURL, _ = url.ParseRequestURI(PushConf.Core.HTTPProxy)
assert.Equal(t, expectedProxyURL, actualProxyURL)
http.DefaultTransport.(*http.Transport).Proxy = nil
}
func TestPushToIOS(t *testing.T) { func TestPushToIOS(t *testing.T) {
PushConf, _ = config.LoadConf("") PushConf, _ = config.LoadConf("")

16
main.go
View File

@ -26,7 +26,6 @@ func main() {
topic string topic string
message string message string
token string token string
proxy string
title string title string
) )
@ -57,7 +56,7 @@ func main() {
flag.BoolVar(&opts.Ios.Enabled, "ios", false, "send ios notification") flag.BoolVar(&opts.Ios.Enabled, "ios", false, "send ios notification")
flag.BoolVar(&opts.Ios.Production, "production", false, "production mode in iOS") flag.BoolVar(&opts.Ios.Production, "production", false, "production mode in iOS")
flag.StringVar(&topic, "topic", "", "apns topic in iOS") flag.StringVar(&topic, "topic", "", "apns topic in iOS")
flag.StringVar(&proxy, "proxy", "", "http proxy url") flag.StringVar(&opts.Core.HTTPProxy, "proxy", "", "http proxy url")
flag.BoolVar(&ping, "ping", false, "ping server") flag.BoolVar(&ping, "ping", false, "ping server")
flag.Usage = usage flag.Usage = usage
@ -113,14 +112,11 @@ func main() {
log.Fatalf("Can't load log module, error: %v", err) log.Fatalf("Can't load log module, error: %v", err)
} }
// set http proxy for GCM if opts.Core.HTTPProxy != "" {
if proxy != "" { gorush.PushConf.Core.HTTPProxy = opts.Core.HTTPProxy
err = gorush.SetProxy(proxy)
if err != nil {
gorush.LogError.Fatalf("Set Proxy error: %v", err)
} }
} else if gorush.PushConf.Core.HTTPProxy != "" {
if gorush.PushConf.Core.HTTPProxy != "" {
err = gorush.SetProxy(gorush.PushConf.Core.HTTPProxy) err = gorush.SetProxy(gorush.PushConf.Core.HTTPProxy)
if err != nil { if err != nil {
@ -268,7 +264,7 @@ Server Options:
-t, --token <token> Notification token -t, --token <token> Notification token
-e, --engine <engine> Storage engine (memory, redis ...) -e, --engine <engine> Storage engine (memory, redis ...)
--title <title> Notification title --title <title> Notification title
--proxy <proxy> Proxy URL (only for GCM) --proxy <proxy> Proxy URL
--pid <pid path> Process identifier path --pid <pid path> Process identifier path
--redis-addr <redis addr> Redis addr (default: localhost:6379) --redis-addr <redis addr> Redis addr (default: localhost:6379)
--ping healthy check command for container --ping healthy check command for container