upgrade apns2 to 0.13 (#297)

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
Bo-Yi Wu
2017-10-24 09:19:18 -05:00
committed by GitHub
parent 115ee18560
commit 34588dd436
22 changed files with 1757 additions and 18 deletions

View File

@@ -1,5 +1,7 @@
# APNS/2
NOTE: This is an experimental branch for the purpose of testing the new token based authentication
APNS/2 is a go package designed for simple, flexible and fast Apple Push Notifications on iOS, OSX and Safari using the new HTTP/2 Push provider API.
[![Build Status](https://travis-ci.org/sideshow/apns2.svg?branch=master)](https://travis-ci.org/sideshow/apns2) [![Coverage Status](https://coveralls.io/repos/sideshow/apns2/badge.svg?branch=master&service=github)](https://coveralls.io/github/sideshow/apns2?branch=master) [![GoDoc](https://godoc.org/github.com/sideshow/apns2?status.svg)](https://godoc.org/github.com/sideshow/apns2)
@@ -7,29 +9,29 @@ APNS/2 is a go package designed for simple, flexible and fast Apple Push Notific
## Features
- Uses new Apple APNs HTTP/2 connection
- Fast - See [notes on speed](https://github.com/sideshow/apns2/wiki/APNS-HTTP-2-Push-Speed)
- Works with go 1.6 and later
- Supports new Apple Token Based Authentication (JWT)
- Supports new iOS 10 features such as Collapse IDs, Subtitles and Mutable Notifications
- Supports persistent connections to APNs
- Supports VoIP/PushKit notifications (iOS 8 and later)
- Fast, modular & easy to use
- Modular & easy to use
- Tested and working in APNs production environment
## Install
- Make sure you have [Go](https://golang.org/doc/install) installed and have set your [GOPATH](https://golang.org/doc/code.html#GOPATH).
- Download and install the dependencies:
```sh
go get -u golang.org/x/net/http2
go get -u golang.org/x/crypto/pkcs12
```
- Install apns2:
```sh
go get -u github.com/sideshow/apns2
```
If you are running the test suite you will also need to install testify:
```sh
go get -u github.com/stretchr/testify
```
## Example
```go
@@ -66,6 +68,34 @@ func main() {
}
```
## JWT Token Example
Instead of using a `.p12` or `.pem` certificate as above, you can optionally use
APNs JWT _Provider Authentication Tokens_. First you will need a signing key (`.p8` file), Key ID and Team ID [from Apple](http://help.apple.com/xcode/mac/current/#/dev54d690a66). Once you have these details, you can create a new client:
```go
authKey, err := token.AuthKeyFromFile("../AuthKey_XXX.p8")
if err != nil {
log.Fatal("token error:", err)
}
token := &token.Token{
AuthKey: authKey,
// KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
KeyID: "ABC123DEFG",
// TeamID from developer account (View Account -> Membership)
TeamID: "DEF123GHIJ",
}
...
client := apns2.NewTokenClient(token)
res, err := client.Push(notification)
```
- You can use one APNs signing key to authenticate tokens for multiple apps.
- A signing key works for both the development and production environments.
- A signing key doesnt expire but can be revoked.
## Notification
At a minimum, a _Notification_ needs a _DeviceToken_, a _Topic_ and a _Payload_.
@@ -138,6 +168,16 @@ res, err := client.PushWithContext(ctx, notification)
defer cancel()
```
## Speed & Performance
Also see the wiki page on [APNS HTTP 2 Push Speed](https://github.com/sideshow/apns2/wiki/APNS-HTTP-2-Push-Speed).
For best performance, you should hold on to an `apns2.Client` instance and not re-create it every push. The underlying TLS connection itself can take a few seconds to connect and negotiate, so if you are setting up an `apns2.Client` and tearing it down every push, then this will greatly affect performance. (Apple suggest keeping the connection open all the time).
You should also limit the amount of `apns2.Client` instances. The underlying transport has a http connection pool itself, so a single client instance will be enough for most users (One instance can potentially do 4,000+ pushes per second). If you need more than this then one instance per CPU core is a good starting point.
Speed is greatly affected by the location of your server and the quality of your network connection. If you're just testing locally, behind a proxy or if your server is outside USA then you're not going to get great performance. With a good server located in AWS, you should be able to get [decent throughput](https://github.com/sideshow/apns2/wiki/APNS-HTTP-2-Push-Speed).
## Command line tool
APNS/2 has a command line tool that can be installed with `go get github.com/sideshow/apns2/apns2`. Usage:

View File

@@ -13,6 +13,7 @@ import (
"net/http"
"time"
"github.com/sideshow/apns2/token"
"golang.org/x/net/http2"
)
@@ -33,13 +34,31 @@ var (
// HTTPClient. The timeout includes connection time, any redirects,
// and reading the response body.
HTTPClientTimeout = 60 * time.Second
// TCPKeepAlive specifies the keep-alive period for an active network
// connection. If zero, keep-alives are not enabled.
TCPKeepAlive = 60 * time.Second
)
// DialTLS is the default dial function for creating TLS connections for
// non-proxied HTTPS requests.
var DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{
Timeout: TLSDialTimeout,
KeepAlive: TCPKeepAlive,
}
return tls.DialWithDialer(dialer, network, addr, cfg)
}
// Client represents a connection with the APNs
type Client struct {
HTTPClient *http.Client
Certificate tls.Certificate
Host string
Certificate tls.Certificate
Token *token.Token
HTTPClient *http.Client
}
type connectionCloser interface {
CloseIdleConnections()
}
// NewClient returns a new Client with an underlying http.Client configured with
@@ -62,9 +81,7 @@ func NewClient(certificate tls.Certificate) *Client {
}
transport := &http2.Transport{
TLSClientConfig: tlsConfig,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return tls.DialWithDialer(&net.Dialer{Timeout: TLSDialTimeout}, network, addr, cfg)
},
DialTLS: DialTLS,
}
return &Client{
HTTPClient: &http.Client{
@@ -76,6 +93,28 @@ func NewClient(certificate tls.Certificate) *Client {
}
}
// NewTokenClient returns a new Client with an underlying http.Client configured
// with the correct APNs HTTP/2 transport settings. It does not connect to the APNs
// until the first Notification is sent via the Push method.
//
// As per the Apple APNs Provider API, you should keep a handle on this client
// so that you can keep your connections with APNs open across multiple
// notifications; dont repeatedly open and close connections. APNs treats rapid
// connection and disconnection as a denial-of-service attack.
func NewTokenClient(token *token.Token) *Client {
transport := &http2.Transport{
DialTLS: DialTLS,
}
return &Client{
Token: token,
HTTPClient: &http.Client{
Transport: transport,
Timeout: HTTPClientTimeout,
},
Host: DefaultHost,
}
}
// Development sets the Client to use the APNs development push endpoint.
func (c *Client) Development() *Client {
c.Host = HostDevelopment
@@ -116,6 +155,11 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error
url := fmt.Sprintf("%v/3/device/%v", c.Host, n.DeviceToken)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
if c.Token != nil {
c.setTokenHeader(req)
}
setHeaders(req, n)
httpRes, err := c.requestWithContext(ctx, req)
@@ -135,6 +179,18 @@ func (c *Client) PushWithContext(ctx Context, n *Notification) (*Response, error
return response, nil
}
// CloseIdleConnections closes any underlying connections which were previously
// connected from previous requests but are now sitting idle. It will not
// interrupt any connections currently in use.
func (c *Client) CloseIdleConnections() {
c.HTTPClient.Transport.(connectionCloser).CloseIdleConnections()
}
func (c *Client) setTokenHeader(r *http.Request) {
c.Token.GenerateIfExpired()
r.Header.Set("authorization", fmt.Sprintf("bearer %v", c.Token.Bearer))
}
func setHeaders(r *http.Request, n *Notification) {
r.Header.Set("Content-Type", "application/json; charset=utf-8")
if n.Topic != "" {

108
vendor/github.com/sideshow/apns2/token/token.go generated vendored Normal file
View File

@@ -0,0 +1,108 @@
package token
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"sync"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
const (
// TokenTimeout is the period of time in seconds that a token is valid for.
// If the timestamp for token issue is not within the last hour, APNs
// rejects subsequent push messages. This is set to under an hour so that
// we generate a new token before the existing one expires.
TokenTimeout = 3000
)
// Possible errors when parsing a .p8 file.
var (
ErrAuthKeyNotPem = errors.New("token: AuthKey must be a valid .p8 PEM file")
ErrAuthKeyNotECDSA = errors.New("token: AuthKey must be of type ecdsa.PrivateKey")
ErrAuthKeyNil = errors.New("token: AuthKey was nil")
)
// Token represents an Apple Provider Authentication Token (JSON Web Token).
type Token struct {
sync.Mutex
AuthKey *ecdsa.PrivateKey
KeyID string
TeamID string
IssuedAt int64
Bearer string
}
// AuthKeyFromFile loads a .p8 certificate from a local file and returns a
// *ecdsa.PrivateKey.
func AuthKeyFromFile(filename string) (*ecdsa.PrivateKey, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return AuthKeyFromBytes(bytes)
}
// AuthKeyFromBytes loads a .p8 certificate from an in memory byte array and
// returns an *ecdsa.PrivateKey.
func AuthKeyFromBytes(bytes []byte) (*ecdsa.PrivateKey, error) {
block, _ := pem.Decode(bytes)
if block == nil {
return nil, ErrAuthKeyNotPem
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
switch pk := key.(type) {
case *ecdsa.PrivateKey:
return pk, nil
default:
return nil, ErrAuthKeyNotECDSA
}
}
// GenerateIfExpired checks to see if the token is about to expire and
// generates a new token.
func (t *Token) GenerateIfExpired() {
t.Lock()
defer t.Unlock()
if t.Expired() {
t.Generate()
}
}
// Expired checks to see if the token has expired.
func (t *Token) Expired() bool {
return time.Now().Unix() >= (t.IssuedAt + TokenTimeout)
}
// Generate creates a new token.
func (t *Token) Generate() (bool, error) {
if t.AuthKey == nil {
return false, ErrAuthKeyNil
}
issuedAt := time.Now().Unix()
jwtToken := &jwt.Token{
Header: map[string]interface{}{
"alg": "ES256",
"kid": t.KeyID,
},
Claims: jwt.MapClaims{
"iss": t.TeamID,
"iat": issuedAt,
},
Method: jwt.SigningMethodES256,
}
bearer, err := jwtToken.SignedString(t.AuthKey)
if err != nil {
return false, err
}
t.IssuedAt = issuedAt
t.Bearer = bearer
return true, nil
}