Skip to content

Commit

Permalink
Merge pull request #47 from kayac/feature/provider-authentication-tok…
Browse files Browse the repository at this point in the history
…en/master

Feature/provider authentication token/master
  • Loading branch information
takyoshi authored Aug 30, 2018
2 parents 69ef8fe + dad2fed commit 4ead54f
Show file tree
Hide file tree
Showing 25 changed files with 754 additions and 296 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ _testmain.go
*.prof

# User specific
/conf/gunfish.toml
/config/gunfish.toml
/gunfish
/shotgun
/gunfish-cli
/apnsmock
/test/server*
/h2o_access.log
/pkg
10 changes: 0 additions & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,18 @@ notifications:

env:
global:
- H2O_VERSION=1.6.3
- PROJECT_ROOT=$HOME/gopath/src/github.com/kayac/Gunfish

go:
- 1.9.x
- 1.10.3
- tip

addons:
apt:
sources:
- kalakris-cmake
packages:
- cmake
- curl

install:
- cd $PROJECT_ROOT && ./test/scripts/build_h2o.sh
- cd $PROJECT_ROOT && make get-dep-on-ci && make get-deps
- go get github.com/mitchellh/gox

Expand All @@ -44,7 +38,3 @@ deploy:
on:
tags: true
go: 1.10.3

cache:
directories:
- $PROJECT_ROOT/h2o-$H2O_VERSION
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ gen-cert:
test/scripts/gen_test_cert.sh

test: gen-cert
nohup h2o -c conf/h2o/h2o.conf > h2o_access.log &
go test -v ./apns || ( pkill h2o && exit 1 )
go test -v ./fcm || ( pkill h2o && exit 1 )
go test -v . || ( pkill h2o && exit 1 )
pkill h2o
go test -v ./apns
go test -v ./fcm
go test -v .

clean:
rm -f cmd/gunfish/gunfish
Expand All @@ -44,3 +42,6 @@ clean:

build:
go build -gcflags="-trimpath=${HOME}" -ldflags="-w" cmd/gunfish/gunfish.go

tools/%:
go build -gcflags="-trimpath=${HOME}" -ldflags="-w" test/tools/$*/$*.go
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ max_connections = 2000
error_hook = "echo -e 'Hello Gunfish at error hook!'"

[apns]
skip_insecure = true
key_file = "/path/to/server.key"
cert_file = "/path/to/server.crt"

Expand All @@ -186,7 +185,6 @@ worker_num |optional| Number of Gunfish owns http clients.
queue_size |optional| Limit number of posted JSON from the developer application.
max_request_size |optional| Limit size of Posted JSON array.
max_connections |optional| Max connections
skip_insecure |optional| Controls whether a client verifies the server's certificate chain and host name.
key_file |required| The key file path.
cert_file |required| The cert file path.
error_hook |optional| Error hook command. This command runs when Gunfish catches an error response.
Expand Down Expand Up @@ -291,20 +289,37 @@ InitErrorResponseHandler(CustomYourErrorHandler{hookCmd: "echo 'on error!'"})
You can implement a success custom handler in the same way but a hook command is not executed in the success handler in order not to make cpu resource too tight.

### Test
To do test for Gunfish, you have to install [h2o](https://h2o.examp1e.net/). **h2o** is used as APNS mock server. So, if you want to test or optimize parameters for your application, you need to prepare the envronment that h2o APNs Mock server works.

Moreover, you have to build h2o with **mruby-sleep** mrbgem.
```
$ make test
```

The following tools are useful to send requests to gunfish for test the following.
- gunfish-cli (send push notification to Gunfish for test)
- apnsmock (APNs mock server)

```
$ make test
$ make tools/gunfish-cli
$ make tools/apnsmock
```

- send a request example with gunfish-cli
```
$ ./gunfish-cli -type apns -count 1 -json-file some.json -verbose
$ ./gunfish-cli -type apns -count 1 -token <device token> -apns-topic <your topic> -options key1=val1,key2=val2 -verbose
```

- start apnsmock server
```
$ ./apnsmock -cert-file ./test/server.crt -key-file ./test/server.key -verbose
```

### Benchmark
Gunfish repository includes Lua script for the benchmark. You can use wrk command with `err_and_success.lua` script.

```
$ h2o -c conf/h2o/h2o.conf &
$ make tools/apnsmock
$ ./apnsmock -cert-file ./test/server.crt -key-file ./test/server.key -verbosea &
$ ./gunfish -c test/gunfish_test.toml -E test
$ wrk2 -t2 -c20 -s bench/scripts/err_and_success.lua -L -R100 http://localhost:38103
```
109 changes: 78 additions & 31 deletions apns/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/url"
"time"

"github.com/kayac/Gunfish/config"
"golang.org/x/net/http2"
)

Expand All @@ -18,10 +19,28 @@ const (
HTTP2ClientTimeout = time.Second * 10
)

var ClientTransport = func(cert tls.Certificate) *http.Transport {
return &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
}
}

type authToken struct {
jwt string
issuedAt time.Time
}

// Client is apns client
type Client struct {
Host string
client *http.Client
Host string
client *http.Client
authToken authToken
kid string
teamID string
key []byte
useAuthToken bool
}

// Send sends notifications to apns
Expand Down Expand Up @@ -90,51 +109,79 @@ func (ac *Client) NewRequest(token string, h *Header, payload Payload) (*http.Re
}
}

// APNs provider token authenticaton
if ac.useAuthToken {
// If iat of jwt is more than 1 hour ago, returns 403 InvalidProviderToken.
// So, recreate jwt earlier than 1 hour.
if ac.authToken.issuedAt.Add(time.Hour - time.Minute).Before(time.Now()) {
if err := ac.issueToken(); err != nil {
return nil, err
}
}
nreq.Header.Set("Authorization", "bearer "+ac.authToken.jwt)
}

return nreq, err
}

// NewConnection establishes a http2 connection
func NewConnection(certFile, keyFile string, secuskip bool) (*http.Client, error) {
certPEMBlock, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
func (ac *Client) issueToken() error {
var err error
now := time.Now()

keyPEMBlock, err := ioutil.ReadFile(keyFile)
ac.authToken.jwt, err = CreateJWT(ac.key, ac.kid, ac.teamID, now)
if err != nil {
return nil, err
return err
}
ac.authToken.issuedAt = now
return nil
}

cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
func NewClient(conf config.SectionApns) (*Client, error) {
useAuthToken := conf.Kid != "" && conf.TeamID != ""
tr := &http.Transport{}
if !useAuthToken {
certPEMBlock, err := ioutil.ReadFile(conf.CertFile)
if err != nil {
return nil, err
}

if err != nil {
return nil, err
}
keyPEMBlock, err := ioutil.ReadFile(conf.KeyFile)
if err != nil {
return nil, err
}

tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: secuskip,
Certificates: []tls.Certificate{cert},
},
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, err
}
tr = ClientTransport(cert)
}

if err := http2.ConfigureTransport(tr); err != nil {
return nil, err
}

return &http.Client{
Timeout: HTTP2ClientTimeout,
Transport: tr,
}, nil
}

func NewClient(host, cert, key string, skipInsecure bool) (*Client, error) {
c, err := NewConnection(cert, key, skipInsecure)
key, err := ioutil.ReadFile(conf.KeyFile)
if err != nil {
return nil, err
}
return &Client{
Host: host,
client: c,
}, nil

client := &Client{
Host: conf.Host,
client: &http.Client{
Timeout: HTTP2ClientTimeout,
Transport: tr,
},
kid: conf.Kid,
teamID: conf.TeamID,
key: key,
useAuthToken: useAuthToken,
}
if client.useAuthToken {
if err := client.issueToken(); err != nil {
return nil, err
}
}

return client, nil
}
112 changes: 112 additions & 0 deletions apns/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package apns

import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"math/big"
"time"
)

// https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1

const jwtDefaultGrowSize = 256

type jwtHeader struct {
Alg string `json:"alg"`
Kid string `json:"kid"`
}

type jwtClaim struct {
Iss string `json:"iss"`
Iat int64 `json:"iat"`
}

type ecdsaSignature struct {
R, S *big.Int
}

func CreateJWT(key []byte, kid string, teamID string, now time.Time) (string, error) {
var b bytes.Buffer
b.Grow(jwtDefaultGrowSize)

header := jwtHeader{
Alg: "ES256",
Kid: kid,
}
headerJSON, err := json.Marshal(&header)
if err != nil {
return "", err
}
if err := writeAsBase64(&b, headerJSON); err != nil {
return "", err
}
b.WriteByte(byte('.'))

claim := jwtClaim{
Iss: teamID,
Iat: now.Unix(),
}
claimJSON, err := json.Marshal(&claim)
if err != nil {
return "", err
}
if err := writeAsBase64(&b, claimJSON); err != nil {
return "", err
}

sig, err := createSignature(b.Bytes(), key)
if err != nil {
return "", err
}
b.WriteByte(byte('.'))

if err := writeAsBase64(&b, sig); err != nil {
return "", err
}

return b.String(), nil
}

func writeAsBase64(w io.Writer, byt []byte) error {
enc := base64.NewEncoder(base64.RawURLEncoding, w)
defer enc.Close()

if _, err := enc.Write(byt); err != nil {
return err
}
return nil
}

func createSignature(payload []byte, key []byte) ([]byte, error) {
h := crypto.SHA256.New()
if _, err := h.Write(payload); err != nil {
return nil, err
}
msg := h.Sum(nil)

block, _ := pem.Decode(key)
p8key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}

r, s, err := ecdsa.Sign(rand.Reader, p8key.(*ecdsa.PrivateKey), msg)
if err != nil {
return nil, err
}

sig, err := asn1.Marshal(ecdsaSignature{r, s})
if err != nil {
return nil, err
}

return sig, nil
}
Loading

0 comments on commit 4ead54f

Please sign in to comment.