diff --git a/.gitignore b/.gitignore index 3821106..0236928 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -.vendor -_vendor -vendor bin/gaurun bin/gaurun_client bin/gaurun_recover diff --git a/go.mod b/go.mod index 870a8f9..0f6d3c3 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( github.com/pkg/errors v0.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 - go.uber.org/atomic v1.1.0 + go.uber.org/atomic v1.1.0 // indirect go.uber.org/zap v0.0.0-20170224221842-12592ca48efc golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 // indirect golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 ) -replace github.com/RobotsAndPencils/buford => github.com/flexfrank/buford v0.13.1-0.20190906024551-21672ff2794e +replace github.com/RobotsAndPencils/buford => ./vendor/buford diff --git a/go.sum b/go.sum index 3e4cb51..172ee84 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,9 @@ github.com/BurntSushi/toml v0.2.0 h1:OthAm9ZSUx4uAmn3WbPwc06nowWrByRwBsYRhbmFjBs= github.com/BurntSushi/toml v0.2.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/RobotsAndPencils/buford v0.12.0 h1:2nfOk+N/QVoQHwXIS0m5TFdvlUjEnqAj/0yXKR75azY= -github.com/RobotsAndPencils/buford v0.12.0/go.mod h1:27KhJZ/wLQHRnsZF+mTWKvF5w8U4dVl4Nh+BfQem4Lo= github.com/client9/reopen v0.0.0-20160619053521-4b86f9c0ead5 h1:46QA9E5dIKm6lNygBd+eSiLF32HsgjJwbA1IELhs5Vo= github.com/client9/reopen v0.0.0-20160619053521-4b86f9c0ead5/go.mod h1:caXVCEr+lUtoN1FlsRiOWdfQtdRHIYfcb0ai8qKWtkQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/flexfrank/buford v0.13.1-0.20190906024551-21672ff2794e h1:6pcQA9RJaK0CtoW/BB58Je4BT916gyH5hCWSH2s+B7o= -github.com/flexfrank/buford v0.13.1-0.20190906024551-21672ff2794e/go.mod h1:9w6wdgYczkqCGQWRaV0BB7ygAqNgcnRTHX+1w4E3OWc= github.com/fukata/golang-stats-api-handler v0.0.0-20160325105040-ab9f90f16caa h1:YxLexpeQS0Fz/sNa3QQEgLWyVhgNhEB1GQUi+c45nFs= github.com/fukata/golang-stats-api-handler v0.0.0-20160325105040-ab9f90f16caa/go.mod h1:1sIi4/rHq6s/ednWMZqTmRq3765qTUSs/c3xF6lj8J8= github.com/lestrrat/go-server-starter v0.0.0-20151125041704-901cec093d58 h1:9/ngkqJb42WLtYR6EFRnImztkmF+n5EhwKBxVOj2B3o= @@ -25,8 +21,6 @@ go.uber.org/zap v0.0.0-20170224221842-12592ca48efc/go.mod h1:vwi/ZaCAaUcBkycHslx golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/net v0.0.0-20161116075034-4971afdc2f16 h1:x2xFZACPoDbV+g+48fDH/4EQTTNPgHTRko7g0JQiZws= -golang.org/x/net v0.0.0-20161116075034-4971afdc2f16/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/vendor/buford/.github/ISSUE_TEMPLATE.md b/vendor/buford/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..650f99c --- /dev/null +++ b/vendor/buford/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ +Please ensure that you are using the latest version of Buford and its dependencies before reporting an issue. Use the `-u` flag with `go get` as described in the [README](https://github.com/RobotsAndPencils/buford/blob/master/README.md#installation). + +NOTE: please don't include your device token or certificate password in issues. + +1. What version of Go are you using (`go version`)? +2. What operating system (GOOS) are you using (`go env`) and what version? +3. What did you do? (steps to reproduce or a code sample is helpful) +4. What did you expect to see? +5. What did you see instead? diff --git a/vendor/buford/.gitignore b/vendor/buford/.gitignore new file mode 100644 index 0000000..d340284 --- /dev/null +++ b/vendor/buford/.gitignore @@ -0,0 +1,9 @@ +*.p12 +*.pem +*.cer +c.out +*.pass +*.pkpass +!testdata/* +*.sublime-project +coverage.txt diff --git a/vendor/buford/.travis.yml b/vendor/buford/.travis.yml new file mode 100644 index 0000000..528b3a7 --- /dev/null +++ b/vendor/buford/.travis.yml @@ -0,0 +1,33 @@ +sudo: false +language: go + +go: + - 1.7.1 + - 1.6.3 + - "1.10" + - tip + +matrix: + allow_failures: + - go: tip + fast_finish: true + +before_script: + - go get -u golang.org/x/lint/golint + +script: + - ./test.sh + +after_script: + - test -z "$(gofmt -s -l -w . | tee /dev/stderr)" + - test -z "$(golint ./... | tee /dev/stderr)" + - go vet ./... + +after_success: + - bash <(curl -s https://codecov.io/bash) + +os: + - linux + +notifications: + email: false diff --git a/vendor/buford/LICENSE b/vendor/buford/LICENSE new file mode 100644 index 0000000..6ee2156 --- /dev/null +++ b/vendor/buford/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Robots and Pencils + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/buford/README.md b/vendor/buford/README.md new file mode 100644 index 0000000..56f0420 --- /dev/null +++ b/vendor/buford/README.md @@ -0,0 +1,234 @@ +# Buford + +Apple Push Notification (APN) Provider library for Go 1.6 and HTTP/2. Send remote notifications to iOS, macOS, tvOS and watchOS. Buford can also sign push packages for Safari notifications and Wallet passes. + +Please see [releases](https://github.com/RobotsAndPencils/buford/releases) for updates. + +[![GoDoc](https://godoc.org/github.com/RobotsAndPencils/buford?status.svg)](https://godoc.org/github.com/RobotsAndPencils/buford) [![Build Status](https://travis-ci.org/RobotsAndPencils/buford.svg?branch=ci)](https://travis-ci.org/RobotsAndPencils/buford) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![codecov](https://codecov.io/gh/RobotsAndPencils/buford/branch/master/graph/badge.svg)](https://codecov.io/gh/RobotsAndPencils/buford) + +### Documentation + +Buford uses Apple's new HTTP/2 Notification API that was announced at WWDC 2015 and [released on December 17, 2015](https://developer.apple.com/news/?id=12172015b). + +[API documentation](https://godoc.org/github.com/RobotsAndPencils/buford/) is available from GoDoc. + +Also see Apple's [Local and Remote Notification Programming Guide][notification], especially the sections on the JSON [payload][] and the [Notification API][notification-api]. + +[notification]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/Introduction.html +[payload]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/TheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH107-SW1 +[notification-api]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH101-SW1 + +#### Terminology + +**APN** Apple Push Notification + +**Provider** The Buford library is used to create a _provider_ of push notifications. + +**Service** Apple's push notification service that Buford communicates with. + +**Client** An `http.Client` provides an HTTP/2 client to communicate with the APN Service. + +**Notification** A payload, device token, and headers. + +**Device Token** An identifier for an application on a given device. + +**Payload** The JSON sent to a device. + +**Headers** HTTP/2 headers are used to set priority and expiration. + +### Installation + +This library requires [Go 1.6.3](https://golang.org/dl/) or better. + +``` +go get -u -d github.com/RobotsAndPencils/buford +``` + +Buford depends on several packages outside of the standard library, including the http2 package. Its certificate package depends on the pkcs12 and pushpackage depends on pkcs7. They can be retrieved or updated with: + +``` +go get -u golang.org/x/net/http2 +go get -u golang.org/x/crypto/pkcs12 +go get -u github.com/aai/gocrypto/pkcs7 +``` + +I am still looking for feedback on the API so it may change. Please copy Buford and its dependencies into a `vendor/` folder at the root of your project. + +### Examples + +```go +package main + +import ( + "encoding/json" + "fmt" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/payload" + "github.com/RobotsAndPencils/buford/payload/badge" + "github.com/RobotsAndPencils/buford/push" +) + +// set these variables appropriately +const ( + filename = "/path/to/certificate.p12" + password = "" + host = push.Development + deviceToken = "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433" +) + +func main() { + // load a certificate and use it to connect to the APN service: + cert, err := certificate.Load(filename, password) + exitOnError(err) + + client, err := push.NewClient(cert) + exitOnError(err) + + service := push.NewService(client, host) + + // construct a payload to send to the device: + p := payload.APS{ + Alert: payload.Alert{Body: "Hello HTTP/2"}, + Badge: badge.New(42), + } + b, err := json.Marshal(p) + exitOnError(err) + + // push the notification: + id, err := service.Push(deviceToken, nil, b) + exitOnError(err) + + fmt.Println("apns-id:", id) +} +``` + +See `example/push` for the complete listing. + +#### Concurrent use + +HTTP/2 can send multiple requests over a single connection, but `service.Push` waits for a response before returning. Instead, you can wrap a `Service` in a queue to handle responses independently, allowing you to send multiple notifications at once. + +```go +var wg sync.WaitGroup +queue := push.NewQueue(service, numWorkers) + +// process responses (responses may be received in any order) +go func() { + for resp := range queue.Responses { + log.Println(resp) + // done receiving and processing one response + wg.Done() + } +}() + +// send the notifications +for i := 0; i < 100; i++ { + // increment count of notifications sent and queue it + wg.Add(1) + queue.Push(deviceToken, nil, b) +} + +// wait for all responses to be processed +wg.Wait() +// shutdown the channels and workers for the queue +queue.Close() +``` + +It's important to set up a goroutine to handle responses before sending any notifications, otherwise Push will block waiting for room to return a Response. + +You can configure the number of workers used to send notifications concurrently, but be aware that a larger number isn't necessarily better, as Apple limits the number of concurrent streams. From the Apple Push Notification documentation: + +> "The APNs server allows multiple concurrent streams for each connection. The exact number of streams is based on server load, so do not assume a specific number of streams." + +See `example/concurrent/` for a complete listing. + +#### Headers + +You can specify an ID, expiration, priority, and other parameters via the Headers struct. + +```go +headers := &push.Headers{ + ID: "922D9F1F-B82E-B337-EDC9-DB4FC8527676", + Expiration: time.Now().Add(time.Hour), + LowPriority: true, +} + +id, err := service.Push(deviceToken, headers, b) +``` + +If no ID is specified APNS will generate and return a unique ID. When an expiration is specified, APNS will store and retry sending the notification until that time, otherwise APNS will not store or retry the notification. LowPriority should always be set when sending a ContentAvailable payload. + +#### Custom values + +To add custom values to an APS payload, use the Map method as follows: + +```go +p := payload.APS{ + Alert: payload.Alert{Body: "Message received from Bob"}, +} +pm := p.Map() +pm["acme2"] = []string{"bang", "whiz"} + +b, err := json.Marshal(pm) +if err != nil { + log.Fatal(b) +} + +id, err := service.Push(deviceToken, nil, b) +``` + +#### Error responses + +Errors from `service.Push` or `queue.Response` could be HTTP errors or an error response from Apple. To access the Reason and HTTP Status code, you must convert the `error` to a `push.Error` as follows: + +```go +if e, ok := err.(*push.Error); ok { + switch e.Reason { + case push.ErrBadDeviceToken: + // handle error + } +} +``` + +### Website Push + +Before you can send push notifications through Safari and the Notification Center, you must provide a push package, which is a signed zip file containing some JSON and icons. + +Use `pushpackage` to write a zip to a `http.ResponseWriter` or to a file. It will create the `manifest.json` and `signature` files for you. + +```go +pkg := pushpackage.New(w) +pkg.EncodeJSON("website.json", website) +pkg.File("icon.iconset/icon_128x128@2x.png", "static/icon_128x128@2x.png") +// other icons... (required) +if err := pkg.Sign(cert, nil); err != nil { + log.Fatal(err) +} +``` + +NOTE: The filenames added to the zip may contain forward slashes but not back slashes or drive letters. + +See `example/website/` and the [Safari Push Notifications][safari] documentation. + +[safari]: https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/PushNotifications/PushNotifications.html#//apple_ref/doc/uid/TP40013225-CH3-SW12 + +### Wallet (Passbook) Pass + +A pass is a signed zip file with a .pkpass extension and a `application/vnd.apple.pkpass` MIME type. You can use `pushpackage` to write a .pkpass that contains a `pass.json` file. + +See `example/wallet/` and the [Wallet Developer Guide][wallet]. + +[wallet]: https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/PassKit_PG/index.html + +### Related Projects + +* [apns2](https://github.com/sideshow/apns2) Alternative HTTP/2 APN provider library (Go) +* [go-apns-server](https://github.com/CleverTap/go-apns-server) Mock APN server (Go) +* [gorush](https://github.com/appleboy/gorush) A push notification server (Go) +* [Push Encryption](https://github.com/GoogleChrome/push-encryption-go) Web Push for Chrome and Firefox (Go) +* [micromdm](https://micromdm.io/) Mobile Device Management server (Go) +* [Lowdown](https://github.com/alloy/lowdown) (Ruby) +* [Apnotic](https://github.com/ostinelli/apnotic) (Ruby) +* [Pigeon](https://github.com/codedge-llc/pigeon) (Elixir, iOS and Android) +* [APNSwift](https://github.com/kaunteya/APNSwift) (Swift) diff --git a/vendor/buford/certificate/cert.go b/vendor/buford/certificate/cert.go new file mode 100644 index 0000000..ab2e032 --- /dev/null +++ b/vendor/buford/certificate/cert.go @@ -0,0 +1,91 @@ +// Package certificate loads Push Services certificates exported from your +// Keychain in Personal Information Exchange format (*.p12). +// +// If you prefer to use *.PEM files, you can of course use tls.LoadX509KeyPair +// or tls.X509KeyPair from the standard library. +package certificate + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "strings" + + "golang.org/x/crypto/pkcs12" +) + +// Certificate errors +var ( + ErrExpired = errors.New("certificate has expired or is not yet valid") +) + +// Load a .p12 certificate from disk. +func Load(filename, password string) (tls.Certificate, error) { + p12, err := ioutil.ReadFile(filename) + if err != nil { + return tls.Certificate{}, fmt.Errorf("Unable to load %s: %v", filename, err) + } + return Decode(p12, password) +} + +// Decode and verify an in memory .p12 certificate (DER binary format). +func Decode(p12 []byte, password string) (tls.Certificate, error) { + // decode an x509.Certificate to verify + privateKey, cert, err := pkcs12.Decode(p12, password) + if err != nil { + return tls.Certificate{}, err + } + if err := verify(cert); err != nil { + return tls.Certificate{}, err + } + + // wraps x509 certificate as a tls.Certificate: + return tls.Certificate{ + Certificate: [][]byte{cert.Raw}, + PrivateKey: privateKey, + Leaf: cert, + }, nil +} + +// TopicFromCert extracts topic from a certificate's common name. +func TopicFromCert(cert tls.Certificate) string { + commonName := cert.Leaf.Subject.CommonName + + var topic string + // Apple Push Services: {bundle} + // Apple Development IOS Push Services: {bundle} + n := strings.Index(commonName, ":") + if n != -1 { + topic = strings.TrimSpace(commonName[n+1:]) + } + return topic +} + +// verify checks if a certificate has expired +func verify(cert *x509.Certificate) error { + _, err := cert.Verify(x509.VerifyOptions{}) + if err == nil { + return nil + } + + switch e := err.(type) { + case x509.CertificateInvalidError: + switch e.Reason { + case x509.Expired: + return ErrExpired + case x509.IncompatibleUsage: + // Apple cert fail on go 1.10 + return nil + default: + return err + } + case x509.UnknownAuthorityError: + // Apple cert isn't in the cert pool + // ignoring this error + return nil + default: + return err + } +} diff --git a/vendor/buford/certificate/cert_test.go b/vendor/buford/certificate/cert_test.go new file mode 100644 index 0000000..173a377 --- /dev/null +++ b/vendor/buford/certificate/cert_test.go @@ -0,0 +1,49 @@ +package certificate_test + +import ( + "testing" + + "github.com/RobotsAndPencils/buford/certificate" +) + +func TestValidCert(t *testing.T) { + const name = "../testdata/cert.p12" + + _, err := certificate.Load(name, "") + if err != nil { + t.Fatal(err) + } +} + +func TestExpiredCert(t *testing.T) { + // TODO: figure out how to test certificate loading and validation in CI + const name = "../cert-expired.p12" + + _, err := certificate.Load(name, "") + if err != certificate.ErrExpired { + t.Fatal("Expected expired cert error, got", err) + } +} + +func TestMissingFile(t *testing.T) { + _, err := certificate.Load("hide-and-seek.p12", "") + if err == nil { + t.Fatal("Expected file not found, got", err) + } +} + +func TestTopicFromCert(t *testing.T) { + const name = "../testdata/cert.p12" + + cert, err := certificate.Load(name, "") + if err != nil { + t.Fatal(err) + } + + // TODO: need a test cert with a CommonName + const expected = "" + actual := certificate.TopicFromCert(cert) + if actual != expected { + t.Errorf("Expected topic %q, got %q.", expected, actual) + } +} diff --git a/vendor/buford/doc.go b/vendor/buford/doc.go new file mode 100644 index 0000000..eabf469 --- /dev/null +++ b/vendor/buford/doc.go @@ -0,0 +1,5 @@ +// Package buford is a Go 1.6+ HTTP/2 provider library for the +// Apple Push Notification Service (APNS). +// +// Please see the README for usage. +package buford diff --git a/vendor/buford/example/concurrent/main.go b/vendor/buford/example/concurrent/main.go new file mode 100644 index 0000000..8abf9e6 --- /dev/null +++ b/vendor/buford/example/concurrent/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" + "sync" + "time" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/payload" + "github.com/RobotsAndPencils/buford/push" +) + +func main() { + log.SetFlags(log.Ltime | log.Lmicroseconds) + + var deviceToken, filename, password, environment, host string + var workers uint + var number int + + flag.StringVar(&deviceToken, "d", "", "Device token") + flag.StringVar(&filename, "c", "", "Path to p12 certificate file") + flag.StringVar(&password, "p", "", "Password for p12 file") + flag.StringVar(&environment, "e", "development", "Environment") + flag.UintVar(&workers, "w", 20, "Workers to send notifications") + flag.IntVar(&number, "n", 100, "Number of notifications to send") + flag.Parse() + + // ensure required flags are set: + halt := false + if deviceToken == "" { + fmt.Println("Device token is required.") + halt = true + } + if filename == "" { + fmt.Println("Path to .p12 certificate file is required.") + halt = true + } + switch environment { + case "development": + host = push.Development + case "production": + host = push.Production + default: + fmt.Println("Environment can be development or production.") + halt = true + } + if halt { + flag.Usage() + os.Exit(2) + } + + // load a certificate and use it to connect to the APN service: + cert, err := certificate.Load(filename, password) + exitOnError(err) + + client, err := push.NewClient(cert) + exitOnError(err) + service := push.NewService(client, host) + queue := push.NewQueue(service, workers) + var wg sync.WaitGroup + + // process responses + // NOTE: Responses may be received in any order. + go func() { + count := 1 + for resp := range queue.Responses { + if resp.Err != nil { + log.Printf("(%d) device: %s, error: %v", count, resp.DeviceToken, resp.Err) + } else { + log.Printf("(%d) device: %s, apns-id: %s", count, resp.DeviceToken, resp.ID) + } + count++ + wg.Done() + } + }() + + // prepare notification(s) to send + p := payload.APS{ + Alert: payload.Alert{Body: "Hello HTTP/2"}, + } + b, err := json.Marshal(p) + exitOnError(err) + + // send notifications: + start := time.Now() + for i := 0; i < number; i++ { + wg.Add(1) + queue.Push(deviceToken, nil, b) + } + // done sending notifications, wait for all responses and shutdown: + wg.Wait() + queue.Close() + elapsed := time.Since(start) + + log.Printf("Time for %d responses: %s (%s ea.)", number, elapsed, elapsed/time.Duration(number)) +} + +func exitOnError(err error) { + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/vendor/buford/example/push/main.go b/vendor/buford/example/push/main.go new file mode 100644 index 0000000..e6c03d9 --- /dev/null +++ b/vendor/buford/example/push/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/payload" + "github.com/RobotsAndPencils/buford/payload/badge" + "github.com/RobotsAndPencils/buford/push" +) + +func main() { + var deviceToken, filename, password, environment, host string + + flag.StringVar(&deviceToken, "d", "", "Device token") + flag.StringVar(&filename, "c", "", "Path to .p12 certificate file.") + flag.StringVar(&password, "p", "", "Password for .p12 file.") + flag.StringVar(&environment, "e", "development", "Environment") + flag.Parse() + + // ensure required flags are set: + halt := false + if deviceToken == "" { + fmt.Println("Device token is required.") + halt = true + } + if filename == "" { + fmt.Println("Path to .p12 certificate file is required.") + halt = true + } + switch environment { + case "development": + host = push.Development + case "production": + host = push.Production + default: + fmt.Println("Environment can be development or production.") + halt = true + } + if halt { + flag.Usage() + os.Exit(2) + } + + // load a certificate and use it to connect to the APN service: + cert, err := certificate.Load(filename, password) + exitOnError(err) + + client, err := push.NewClient(cert) + exitOnError(err) + + service := push.NewService(client, host) + + // construct a payload to send to the device: + p := payload.APS{ + Alert: payload.Alert{Body: "Hello HTTP/2"}, + Badge: badge.New(42), + } + b, err := json.Marshal(p) + exitOnError(err) + + // push the notification: + id, err := service.Push(deviceToken, nil, b) + exitOnError(err) + + fmt.Println("apns-id:", id) +} + +func exitOnError(err error) { + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/vendor/buford/example/wallet/README.md b/vendor/buford/example/wallet/README.md new file mode 100644 index 0000000..5c0bb02 --- /dev/null +++ b/vendor/buford/example/wallet/README.md @@ -0,0 +1,18 @@ +# Wallet sign pass + +How to sign a sample pass. + +1. Download the PassKit support materials from the + [Wallet Developer Guide](https://developer.apple.com/library/prerelease/ios/documentation/UserExperience/Conceptual/PassKit_PG/index.html) website. + +2. Unzip WalletCompanionFiles.zip and copy `SamplePasses/Event.pass` into this folder. + +3. Register a Pass Type ID in [Apple's Member Center](https://developer.apple.com/membercenter/index.action). This requires a CSR from your keychain. + +4. Download the `.cer` and add it to Keychain. Then export a `.p12` with the certificate and the private key you generated during step 3. + +5. Modify the passTypeIdentifier and teamIdentifier in pass.json. The teamIdentifier is the Organizational Unit in the .cer file. + +6. Download [Apple Intermediate Certificate](http://www.apple.com/certificateauthority/) WWDR Certificate (Expiring 02/07/23) + +7. Build and run the example with `go run main.go -c /path/to/certificate.p12 -i /path/to/AppleWWDRCA.cer` diff --git a/vendor/buford/example/wallet/main.go b/vendor/buford/example/wallet/main.go new file mode 100644 index 0000000..5365824 --- /dev/null +++ b/vendor/buford/example/wallet/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "crypto/x509" + "flag" + "io/ioutil" + "log" + "os" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/pushpackage" +) + +func loadWWDR(name string) (*x509.Certificate, error) { + b, err := ioutil.ReadFile(name) + if err != nil { + return nil, err + } + return x509.ParseCertificate(b) +} + +func failIfError(err error) { + if err != nil { + log.Fatal(err) + } +} + +func main() { + var filename, password, intermediate string + + flag.StringVar(&filename, "c", "", "Path to p12 certificate file") + flag.StringVar(&password, "p", "", "Password for p12 file.") + flag.StringVar(&intermediate, "i", "", "Path to WWDR intermediate .cer file") + flag.Parse() + + cert, err := certificate.Load(filename, password) + failIfError(err) + + wwdr, err := loadWWDR(intermediate) + failIfError(err) + + f, err := os.Create("Event.pkpass") + failIfError(err) + defer f.Close() + + passFiles := []string{ + "pass.json", + "background.png", + "background@2x.png", + "icon.png", + "icon@2x.png", + "logo.png", + "logo@2x.png", + "thumbnail.png", + "thumbnail@2x.png", + } + + pkg := pushpackage.New(f) + for _, name := range passFiles { + pkg.File(name, "./Event.pass/"+name) + } + + err = pkg.Sign(cert, wwdr) + failIfError(err) +} diff --git a/vendor/buford/example/website/README.md b/vendor/buford/example/website/README.md new file mode 100644 index 0000000..fa40c4d --- /dev/null +++ b/vendor/buford/example/website/README.md @@ -0,0 +1,23 @@ +# Safari Push Notifications + +How to use this example in development. + +1. Create a Website Push ID in [Apple's Member Center](https://developer.apple.com/membercenter/index.action). This requires a CSR from your keychain. + +2. Download the certificate from Apple's website and add it to your Keychain. + +3. Export a `.p12` with the certificate and the private key you generated during step 1. + +4. Update main.go with the PushID you used in step 1. + +5. Download and install [ngrok](https://ngrok.com/) and create an account. + +6. Create a secure tunnel to port 5000 on your local machine by running `ngrok http 5000`. + +7. You can visit [localhost:4040](http://localhost:4040) in your web browser to see requests as they happen. + +8. In ngrok you will see a custom URL like `https://.ngrok.io`. Update AllowedDomains, URLFormatString, and WebServiceURL in main.go based on your custom URL. Be sure to use HTTPS. + +9. Build and run the example with `go run main.go -c /path/to/certificate.p12` + +10. Visit your `https://.ngrok.io` URL in Safari to request permission and then send a push notification, which will appear in your Notification Center. diff --git a/vendor/buford/example/website/index.html b/vendor/buford/example/website/index.html new file mode 100644 index 0000000..4eb7038 --- /dev/null +++ b/vendor/buford/example/website/index.html @@ -0,0 +1,10 @@ + +Safari push index page + + +

Allow push notifications

+ +

Send push

+ + + diff --git a/vendor/buford/example/website/main.go b/vendor/buford/example/website/main.go new file mode 100644 index 0000000..f21288b --- /dev/null +++ b/vendor/buford/example/website/main.go @@ -0,0 +1,167 @@ +package main + +import ( + "crypto/tls" + "encoding/json" + "flag" + "html/template" + "io" + "log" + "net/http" + "strings" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/payload" + "github.com/RobotsAndPencils/buford/push" + "github.com/RobotsAndPencils/buford/pushpackage" + "github.com/gorilla/mux" +) + +var ( + website = pushpackage.Website{ + Name: "Buford", + PushID: "web.com.github.RobotsAndPencils.buford", + AllowedDomains: []string{"https://e31340d3.ngrok.io"}, + URLFormatString: `https://e31340d3.ngrok.io/click?q=%@`, + // AuthenticationToken identifies the user (16+ characters) + AuthenticationToken: "19f8d7a6e9fb8a7f6d9330dabe", + WebServiceURL: "https://e31340d3.ngrok.io", + } + + // Cert for signing push packages. + cert tls.Certificate + + // Service and device token to send push notifications. + service *push.Service + deviceToken string + + templates = template.Must(template.ParseFiles("index.html", "request.html")) +) + +func indexHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "index.html", nil) +} + +func requestPermissionHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "request.html", website) +} + +func pushHandler(w http.ResponseWriter, r *http.Request) { + p := payload.Browser{ + Alert: payload.BrowserAlert{ + Title: "Hello", + Body: "Hello HTTP/2", + }, + // URLArgs must match placeholders in URLFormatString + URLArgs: []string{"hello"}, + } + b, err := json.Marshal(p) + if err != nil { + log.Fatal(err) + } + + id, err := service.Push(deviceToken, nil, b) + if err != nil { + log.Println(err) + return + } + log.Println("apns-id:", id) +} + +func clickHandler(w http.ResponseWriter, r *http.Request) { + log.Println("clicked", r.URL.Query()["q"]) +} + +func pushPackagesHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + log.Println("building push package for", vars["websitePushID"]) + + w.Header().Set("Content-Type", "application/zip") + + // create a push package and sign it with Cert/Key. + pkg := pushpackage.New(w) + pkg.EncodeJSON("website.json", website) + pkg.File("icon.iconset/icon_128x128@2x.png", "../../testdata/gopher.png") + pkg.File("icon.iconset/icon_128x128.png", "../../testdata/gopher.png") + pkg.File("icon.iconset/icon_32x32@2x.png", "../../testdata/gopher.png") + pkg.File("icon.iconset/icon_32x32.png", "../../testdata/gopher.png") + pkg.File("icon.iconset/icon_16x16@2x.png", "../../testdata/gopher.png") + pkg.File("icon.iconset/icon_16x16.png", "../../testdata/gopher.png") + if err := pkg.Sign(cert, nil); err != nil { + log.Fatal(err) + } +} + +func registerDeviceHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + log.Printf("register device %s (user %s) for %s", vars["deviceToken"], getAuthenticationToken(r), vars["websitePushID"]) + + deviceToken = vars["deviceToken"] +} + +func forgetDeviceHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + log.Printf("forget device %s (user %s) for %s", vars["deviceToken"], getAuthenticationToken(r), vars["websitePushID"]) + + deviceToken = "" +} + +func getAuthenticationToken(r *http.Request) string { + h := r.Header.Get("Authorization") + list := strings.SplitN(h, " ", 2) + if len(list) != 2 || list[0] != "ApplePushNotifications" { + return "" + } + return list[1] +} + +func logHandler(w http.ResponseWriter, r *http.Request) { + var logs struct { + Logs []string `json:"logs"` + } + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&logs); err == io.EOF { + return + } else if err != nil { + log.Fatal(err) + } + + for _, msg := range logs.Logs { + log.Println(msg) + } +} + +func main() { + var filename, password string + + flag.StringVar(&filename, "c", "", "Path to p12 certificate file") + flag.StringVar(&password, "p", "", "Password for p12 file.") + flag.Parse() + + var err error + cert, err = certificate.Load(filename, password) + if err != nil { + log.Fatal(err) + } + + client, err := push.NewClient(cert) + if err != nil { + log.Fatal(err) + } + + service = push.NewService(client, push.Production) + + r := mux.NewRouter() + r.HandleFunc("/", indexHandler).Methods("GET") + r.HandleFunc("/request", requestPermissionHandler) + r.HandleFunc("/push", pushHandler) + r.HandleFunc("/click", clickHandler).Methods("GET") + + // WebServiceURL endpoints + r.HandleFunc("/v1/pushPackages/{websitePushID}", pushPackagesHandler).Methods("POST") + r.HandleFunc("/v1/devices/{deviceToken}/registrations/{websitePushID}", registerDeviceHandler).Methods("POST") + r.HandleFunc("/v1/devices/{deviceToken}/registrations/{websitePushID}", forgetDeviceHandler).Methods("DELETE") + r.HandleFunc("/v1/log", logHandler).Methods("POST") + + http.ListenAndServe(":5000", r) +} diff --git a/vendor/buford/example/website/request.html b/vendor/buford/example/website/request.html new file mode 100644 index 0000000..6401235 --- /dev/null +++ b/vendor/buford/example/website/request.html @@ -0,0 +1,39 @@ + +Safari push request permission + + + + + + + diff --git a/vendor/buford/go.mod b/vendor/buford/go.mod new file mode 100644 index 0000000..47f18b1 --- /dev/null +++ b/vendor/buford/go.mod @@ -0,0 +1,3 @@ +module github.com/mercari/gaurun/vendor/buford + +go 1.13 diff --git a/vendor/buford/payload/aps.go b/vendor/buford/payload/aps.go new file mode 100644 index 0000000..3fd601b --- /dev/null +++ b/vendor/buford/payload/aps.go @@ -0,0 +1,123 @@ +package payload + +import ( + "encoding/json" + + "github.com/RobotsAndPencils/buford/payload/badge" +) + +// APS is Apple's reserved namespace. +// Use it for payloads destined to mobile devices (iOS). +type APS struct { + // Alert dictionary. + Alert Alert + + // Badge to display on the app icon. + // Set to badge.Preserve (default), badge.Clear + // or a specific value with badge.New(n). + Badge badge.Badge + + // The name of a sound file to play as an alert. + Sound string + + // Content available is for silent notifications + // with no alert, sound, or badge. + ContentAvailable bool + + // Category identifier for custom actions in iOS 8 or newer. + Category string + + // Mutable is used for Service Extensions introduced in iOS 10. + MutableContent bool + + // Thread identifier to create notification groups in iOS 12 or newer. + ThreadID string +} + +// Alert dictionary. +type Alert struct { + // Title is a short string shown briefly on Apple Watch in iOS 8.2 or newer. + Title string `json:"title,omitempty"` + TitleLocKey string `json:"title-loc-key,omitempty"` + TitleLocArgs []string `json:"title-loc-args,omitempty"` + + // Subtitle added in iOS 10 + Subtitle string `json:"subtitle,omitempty"` + + // Body text of the alert message. + Body string `json:"body,omitempty"` + LocKey string `json:"loc-key,omitempty"` + LocArgs []string `json:"loc-args,omitempty"` + + // Key for localized string for "View" button. + ActionLocKey string `json:"action-loc-key,omitempty"` + + // Image file to be used when user taps or slides the action button. + LaunchImage string `json:"launch-image,omitempty"` +} + +// isSimple alert with only Body set. +func (a *Alert) isSimple() bool { + return len(a.Title) == 0 && len(a.Subtitle) == 0 && + len(a.LaunchImage) == 0 && + len(a.TitleLocKey) == 0 && len(a.TitleLocArgs) == 0 && + len(a.LocKey) == 0 && len(a.LocArgs) == 0 && len(a.ActionLocKey) == 0 +} + +// isZero if no Alert fields are set. +func (a *Alert) isZero() bool { + return len(a.Body) == 0 && a.isSimple() +} + +// Map returns the payload as a map that you can customize +// before serializing it to JSON. +func (a *APS) Map() map[string]interface{} { + aps := make(map[string]interface{}, 5) + + if !a.Alert.isZero() { + if a.Alert.isSimple() { + aps["alert"] = a.Alert.Body + } else { + aps["alert"] = a.Alert + } + } + if n, ok := a.Badge.Number(); ok { + aps["badge"] = n + } + if a.Sound != "" { + aps["sound"] = a.Sound + } + if a.ContentAvailable { + aps["content-available"] = 1 + } + if a.Category != "" { + aps["category"] = a.Category + } + if a.MutableContent { + aps["mutable-content"] = 1 + } + if a.ThreadID != "" { + aps["thread-id"] = a.ThreadID + } + + // wrap in "aps" to form the final payload + return map[string]interface{}{"aps": aps} +} + +// MarshalJSON allows you to json.Marshal(aps) directly. +func (a APS) MarshalJSON() ([]byte, error) { + return json.Marshal(a.Map()) +} + +// Validate that a payload has the correct fields. +func (a *APS) Validate() error { + if a == nil { + return ErrIncomplete + } + + // must have a body or a badge (or custom data) + if len(a.Alert.Body) == 0 && a.Badge == badge.Preserve { + return ErrIncomplete + } + return nil +} diff --git a/vendor/buford/payload/aps_test.go b/vendor/buford/payload/aps_test.go new file mode 100644 index 0000000..05b061b --- /dev/null +++ b/vendor/buford/payload/aps_test.go @@ -0,0 +1,142 @@ +package payload_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/RobotsAndPencils/buford/payload" + "github.com/RobotsAndPencils/buford/payload/badge" +) + +func ExampleAPS() { + p := payload.APS{ + Alert: payload.Alert{Body: "Hello HTTP/2"}, + Badge: badge.New(42), + Sound: "bingbong.aiff", + } + + b, err := json.Marshal(p) + if err != nil { + // handle error + } + fmt.Printf("%s", b) + // Output: {"aps":{"alert":"Hello HTTP/2","badge":42,"sound":"bingbong.aiff"}} +} + +// Use Map to add custom values to the payload. +func ExampleAPS_Map() { + p := payload.APS{ + Alert: payload.Alert{Body: "Topic secret message"}, + } + pm := p.Map() + pm["acme2"] = []string{"bang", "whiz"} + + b, err := json.Marshal(pm) + if err != nil { + // handle error + } + fmt.Printf("%s", b) + // Output: {"acme2":["bang","whiz"],"aps":{"alert":"Topic secret message"}} +} + +func ExampleAPS_Validate() { + p := payload.APS{ + Badge: badge.Preserve, + Sound: "bingbong.aiff", + } + if err := p.Validate(); err != nil { + fmt.Println(err) + } + // Output: payload does not contain necessary fields +} + +func TestPayload(t *testing.T) { + var tests = []struct { + input payload.APS + expected []byte + }{ + { + payload.APS{ + Alert: payload.Alert{Body: "Message received from Bob"}, + }, + []byte(`{"aps":{"alert":"Message received from Bob"}}`), + }, + { + payload.APS{ + Alert: payload.Alert{Body: "You got your emails."}, + Badge: badge.New(9), + Sound: "bingbong.aiff", + }, + []byte(`{"aps":{"alert":"You got your emails.","badge":9,"sound":"bingbong.aiff"}}`), + }, + { + payload.APS{ContentAvailable: true}, + []byte(`{"aps":{"content-available":1}}`), + }, + { + payload.APS{ + Alert: payload.Alert{ + Title: "Message", + Subtitle: "This is important", + Body: "Message received from Bob", + }, + }, + []byte(`{"aps":{"alert":{"title":"Message","subtitle":"This is important","body":"Message received from Bob"}}}`), + }, + { + payload.APS{ + Alert: payload.Alert{Body: "Change is coming"}, + MutableContent: true, + }, + []byte(`{"aps":{"alert":"Change is coming","mutable-content":1}}`), + }, + { + payload.APS{ + Alert: payload.Alert{Body: "Grouped notification"}, + ThreadID: "thread-id-1", + }, + []byte(`{"aps":{"alert":"Grouped notification","thread-id":"thread-id-1"}}`), + }, + } + + for _, tt := range tests { + testPayload(t, tt.input, tt.expected) + } +} + +func TestCustomArray(t *testing.T) { + p := payload.APS{Alert: payload.Alert{Body: "Message received from Bob"}} + pm := p.Map() + pm["acme2"] = []string{"bang", "whiz"} + expected := []byte(`{"acme2":["bang","whiz"],"aps":{"alert":"Message received from Bob"}}`) + testPayload(t, pm, expected) +} + +func TestValidAPS(t *testing.T) { + tests := []payload.APS{ + {Alert: payload.Alert{Body: "You got your emails."}}, + {Badge: badge.New(9)}, + {Badge: badge.Clear}, + } + + for _, p := range tests { + if err := p.Validate(); err != nil { + t.Errorf("Expected no error, got %v.", err) + } + } +} + +func TestInvalidAPS(t *testing.T) { + tests := []*payload.APS{ + {Sound: "bingbong.aiff"}, + {}, + nil, + } + + for _, p := range tests { + if err := p.Validate(); err != payload.ErrIncomplete { + t.Errorf("Expected err %v, got %v.", payload.ErrIncomplete, err) + } + } +} diff --git a/vendor/buford/payload/badge/badge.go b/vendor/buford/payload/badge/badge.go new file mode 100644 index 0000000..33448ef --- /dev/null +++ b/vendor/buford/payload/badge/badge.go @@ -0,0 +1,37 @@ +// Package badge allows you to preserve, set or clear the number displayed +// on your App icon. +package badge + +import "fmt" + +// Badge number to display on the App icon. +type Badge struct { + number uint + isSet bool +} + +// Preserve the current badge (default behavior). +var Preserve = Badge{} + +// Clear the badge. +var Clear = Badge{number: 0, isSet: true} + +// New badge with a set value. +// A badge set to 0 is the same as badge.Clear. +func New(number uint) Badge { + return Badge{number: number, isSet: true} +} + +// Number to display on the App Icon and if should be changed. +// If the badge should not be changed, the number has no effect. +func (b *Badge) Number() (uint, bool) { + return b.number, b.isSet +} + +// String prints out a badge +func (b Badge) String() string { + if b.isSet { + return fmt.Sprintf("set %d", b.number) + } + return "preserve" +} diff --git a/vendor/buford/payload/badge/badge_test.go b/vendor/buford/payload/badge/badge_test.go new file mode 100644 index 0000000..cfb31bc --- /dev/null +++ b/vendor/buford/payload/badge/badge_test.go @@ -0,0 +1,59 @@ +package badge_test + +import ( + "fmt" + "testing" + + "github.com/RobotsAndPencils/buford/payload/badge" +) + +func Example() { + var b badge.Badge + fmt.Println(b) + + fmt.Println(badge.Preserve) + fmt.Println(badge.Clear) + fmt.Println(badge.New(42)) + + // Output: + // preserve + // preserve + // set 0 + // set 42 +} + +func TestDefaultBadge(t *testing.T) { + b := badge.Badge{} + if _, ok := b.Number(); ok { + t.Errorf("Expected badge number to be omitted.") + } +} + +func TestPreserveBadge(t *testing.T) { + b := badge.Preserve + if _, ok := b.Number(); ok { + t.Errorf("Expected badge number to be omitted.") + } +} + +func TestClearBadge(t *testing.T) { + b := badge.Clear + n, ok := b.Number() + if !ok { + t.Errorf("Expected badge to be set for removal.") + } + if n != 0 { + t.Errorf("Expected badge number to be 0, got %d.", n) + } +} + +func TestNewBadge(t *testing.T) { + b := badge.New(4) + n, ok := b.Number() + if !ok { + t.Errorf("Expected badge to be set to change.") + } + if n != 4 { + t.Errorf("Expected badge number to be 4, got %d.", n) + } +} diff --git a/vendor/buford/payload/browser.go b/vendor/buford/payload/browser.go new file mode 100644 index 0000000..485f941 --- /dev/null +++ b/vendor/buford/payload/browser.go @@ -0,0 +1,37 @@ +package payload + +import "encoding/json" + +// Browser for Safari Push Notifications. +type Browser struct { + Alert BrowserAlert + URLArgs []string +} + +// BrowserAlert for Safari Push Notifications. +type BrowserAlert struct { + // Title and Body are required + Title string `json:"title"` + Body string `json:"body"` + // Action button label (defaults to "Show") + Action string `json:"action,omitempty"` +} + +// MarshalJSON allows you to json.Marshal(browser) directly. +func (p Browser) MarshalJSON() ([]byte, error) { + aps := map[string]interface{}{"alert": p.Alert, "url-args": p.URLArgs} + return json.Marshal(map[string]interface{}{"aps": aps}) +} + +// Validate browser payload. +func (p *Browser) Validate() error { + if p == nil { + return ErrIncomplete + } + + // must have both a title and body. action and url-args are optional. + if len(p.Alert.Title) == 0 || len(p.Alert.Body) == 0 { + return ErrIncomplete + } + return nil +} diff --git a/vendor/buford/payload/browser_test.go b/vendor/buford/payload/browser_test.go new file mode 100644 index 0000000..fdc675b --- /dev/null +++ b/vendor/buford/payload/browser_test.go @@ -0,0 +1,68 @@ +package payload_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/RobotsAndPencils/buford/payload" +) + +func ExampleBrowser() { + p := payload.Browser{ + Alert: payload.BrowserAlert{ + Title: "Flight A998 Now Boarding", + Body: "Boarding has begun for Flight A998.", + Action: "View", + }, + URLArgs: []string{"boarding", "A998"}, + } + + b, err := json.Marshal(p) + if err != nil { + // handle error + } + fmt.Printf("%s", b) + // Output: {"aps":{"alert":{"title":"Flight A998 Now Boarding","body":"Boarding has begun for Flight A998.","action":"View"},"url-args":["boarding","A998"]}} +} + +func TestBrowser(t *testing.T) { + p := payload.Browser{ + Alert: payload.BrowserAlert{ + Title: "Flight A998 Now Boarding", + Body: "Boarding has begun for Flight A998.", + Action: "View", + }, + URLArgs: []string{"boarding", "A998"}, + } + expected := []byte(`{"aps":{"alert":{"title":"Flight A998 Now Boarding","body":"Boarding has begun for Flight A998.","action":"View"},"url-args":["boarding","A998"]}}`) + testPayload(t, p, expected) +} + +func TestValidBrowser(t *testing.T) { + p := payload.Browser{ + Alert: payload.BrowserAlert{ + Title: "Flight A998 Now Boarding", + Body: "Boarding has begun for Flight A998.", + }, + } + if err := p.Validate(); err != nil { + t.Errorf("Expected no error, got %v.", err) + } +} + +func TestInvalidBrowser(t *testing.T) { + tests := []*payload.Browser{ + { + Alert: payload.BrowserAlert{Action: "View"}, + }, + {}, + nil, + } + + for _, p := range tests { + if err := p.Validate(); err != payload.ErrIncomplete { + t.Errorf("Expected err %v, got %v.", payload.ErrIncomplete, err) + } + } +} diff --git a/vendor/buford/payload/mdm.go b/vendor/buford/payload/mdm.go new file mode 100644 index 0000000..8156d2e --- /dev/null +++ b/vendor/buford/payload/mdm.go @@ -0,0 +1,19 @@ +package payload + +// MDM payload for mobile device management. +type MDM struct { + Token string `json:"mdm"` +} + +// Validate MDM payload. +func (p *MDM) Validate() error { + if p == nil { + return ErrIncomplete + } + + // must have a token. + if len(p.Token) == 0 { + return ErrIncomplete + } + return nil +} diff --git a/vendor/buford/payload/mdm_test.go b/vendor/buford/payload/mdm_test.go new file mode 100644 index 0000000..22937fb --- /dev/null +++ b/vendor/buford/payload/mdm_test.go @@ -0,0 +1,33 @@ +package payload_test + +import ( + "testing" + + "github.com/RobotsAndPencils/buford/payload" +) + +func TestMDM(t *testing.T) { + p := payload.MDM{Token: "00000000-1111-3333-4444-555555555555"} + expected := []byte(`{"mdm":"00000000-1111-3333-4444-555555555555"}`) + testPayload(t, p, expected) +} + +func TestValidMDM(t *testing.T) { + p := payload.MDM{Token: "00000000-1111-3333-4444-555555555555"} + if err := p.Validate(); err != nil { + t.Errorf("Expected no error, got %v.", err) + } +} + +func TestInvalidMDM(t *testing.T) { + tests := []*payload.MDM{ + {}, + nil, + } + + for _, p := range tests { + if err := p.Validate(); err != payload.ErrIncomplete { + t.Errorf("Expected err %v, got %v.", payload.ErrIncomplete, err) + } + } +} diff --git a/vendor/buford/payload/payload.go b/vendor/buford/payload/payload.go new file mode 100644 index 0000000..08f1ff8 --- /dev/null +++ b/vendor/buford/payload/payload.go @@ -0,0 +1,9 @@ +// Package payload prepares a JSON payload to push. +package payload + +import "errors" + +// Validation errors. +var ( + ErrIncomplete = errors.New("payload does not contain necessary fields") +) diff --git a/vendor/buford/payload/payload_test.go b/vendor/buford/payload/payload_test.go new file mode 100644 index 0000000..567cfa6 --- /dev/null +++ b/vendor/buford/payload/payload_test.go @@ -0,0 +1,17 @@ +package payload_test + +import ( + "encoding/json" + "reflect" + "testing" +) + +func testPayload(t *testing.T, p interface{}, expected []byte) { + b, err := json.Marshal(p) + if err != nil { + t.Fatal("Unexpected error", err) + } + if !reflect.DeepEqual(b, expected) { + t.Errorf("Expected %s, got %s", expected, b) + } +} diff --git a/vendor/buford/push/device_token.go b/vendor/buford/push/device_token.go new file mode 100644 index 0000000..83244dd --- /dev/null +++ b/vendor/buford/push/device_token.go @@ -0,0 +1,13 @@ +package push + +import "encoding/hex" + +// IsDeviceTokenValid checks if s is a hexadecimal token of the correct length. +func IsDeviceTokenValid(s string) bool { + // TODO: In 2016, they may be growing up to 100 bytes (200 hexadecimal digits). + if len(s) < 64 || len(s) > 200 { + return false + } + _, err := hex.DecodeString(s) + return err == nil +} diff --git a/vendor/buford/push/device_token_test.go b/vendor/buford/push/device_token_test.go new file mode 100644 index 0000000..1dfb6e5 --- /dev/null +++ b/vendor/buford/push/device_token_test.go @@ -0,0 +1,36 @@ +package push_test + +import ( + "testing" + + "github.com/RobotsAndPencils/buford/push" +) + +func TestInvalidDeviceTokens(t *testing.T) { + tokens := []string{ + "invalid-token", + "f00f", + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl", + "50c4afb5 9197d2ba d1794be4 e63f2532 ee18c660 0ee655fa 38b0b380 94fd8847", + "<50c4afb5 9197d2ba d1794be4 e63f2532 ee18c660 0ee655fa 38b0b380 94fd8847>", + } + + for _, token := range tokens { + if push.IsDeviceTokenValid(token) { + t.Errorf("Expected device token %q to be invalid.", token) + } + } +} + +func TestValidDeviceToken(t *testing.T) { + tokens := []string{ + "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433", + "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954ac2732227a1d8021cfaf781d71fb2f908c61f5861079a00954ac2732227a1d8021cfaf781d71fb2f908c61f5861079a00954ac2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a", + } + + for _, token := range tokens { + if !push.IsDeviceTokenValid(token) { + t.Errorf("Expected device token %q to be valid.", token) + } + } +} diff --git a/vendor/buford/push/errors.go b/vendor/buford/push/errors.go new file mode 100644 index 0000000..ed73613 --- /dev/null +++ b/vendor/buford/push/errors.go @@ -0,0 +1,167 @@ +package push + +import ( + "errors" + "fmt" + "time" +) + +// Error responses from Apple +type Error struct { + Reason error + Status int // http StatusCode + Timestamp time.Time +} + +// Service error responses. +var ( + // These could be checked prior to sending the request to Apple. + ErrPayloadEmpty = errors.New("PayloadEmpty") + ErrPayloadTooLarge = errors.New("PayloadTooLarge") + + // Device token errors. + ErrMissingDeviceToken = errors.New("MissingDeviceToken") + ErrBadDeviceToken = errors.New("BadDeviceToken") + ErrTooManyRequests = errors.New("TooManyRequests") + + // Header errors. + ErrBadMessageID = errors.New("BadMessageID") + ErrBadExpirationDate = errors.New("BadExpirationDate") + ErrBadPriority = errors.New("BadPriority") + ErrBadTopic = errors.New("BadTopic") + ErrInvalidPushType = errors.New("InvalidPushType") + + // Certificate and topic errors. + ErrBadCertificate = errors.New("BadCertificate") + ErrBadCertificateEnvironment = errors.New("BadCertificateEnvironment") + ErrForbidden = errors.New("Forbidden") + ErrMissingTopic = errors.New("MissingTopic") + ErrTopicDisallowed = errors.New("TopicDisallowed") + ErrUnregistered = errors.New("Unregistered") + ErrDeviceTokenNotForTopic = errors.New("DeviceTokenNotForTopic") + + // These errors should never happen when using Push. + ErrDuplicateHeaders = errors.New("DuplicateHeaders") + ErrBadPath = errors.New("BadPath") + ErrMethodNotAllowed = errors.New("MethodNotAllowed") + + // Fatal server errors. + ErrIdleTimeout = errors.New("IdleTimeout") + ErrShutdown = errors.New("Shutdown") + ErrInternalServerError = errors.New("InternalServerError") + ErrServiceUnavailable = errors.New("ServiceUnavailable") +) + +// mapErrorReason converts Apple error responses into exported Err variables +// for comparisons. +func mapErrorReason(reason string) error { + var e error + switch reason { + case "PayloadEmpty": + e = ErrPayloadEmpty + case "PayloadTooLarge": + e = ErrPayloadTooLarge + case "BadTopic": + e = ErrBadTopic + case "TopicDisallowed": + e = ErrTopicDisallowed + case "BadMessageId": + e = ErrBadMessageID + case "BadExpirationDate": + e = ErrBadExpirationDate + case "BadPriority": + e = ErrBadPriority + case "MissingDeviceToken": + e = ErrMissingDeviceToken + case "BadDeviceToken": + e = ErrBadDeviceToken + case "DeviceTokenNotForTopic": + e = ErrDeviceTokenNotForTopic + case "Unregistered": + e = ErrUnregistered + case "DuplicateHeaders": + e = ErrDuplicateHeaders + case "BadCertificateEnvironment": + e = ErrBadCertificateEnvironment + case "BadCertificate": + e = ErrBadCertificate + case "Forbidden": + e = ErrForbidden + case "BadPath": + e = ErrBadPath + case "MethodNotAllowed": + e = ErrMethodNotAllowed + case "TooManyRequests": + e = ErrTooManyRequests + case "IdleTimeout": + e = ErrIdleTimeout + case "Shutdown": + e = ErrShutdown + case "InternalServerError": + e = ErrInternalServerError + case "ServiceUnavailable": + e = ErrServiceUnavailable + case "MissingTopic": + e = ErrMissingTopic + case "InvalidPushType": + e = ErrInvalidPushType + default: + e = errors.New(reason) + } + return e +} + +func (e *Error) Error() string { + switch e.Reason { + case ErrPayloadEmpty: + return "the message payload was empty" + case ErrPayloadTooLarge: + return "the message payload was too large" + case ErrMissingDeviceToken: + return "device token was not specified" + case ErrBadDeviceToken: + return "bad device token" + case ErrTooManyRequests: + return "too many requests were made consecutively to the same device token" + case ErrBadMessageID: + return "the ID header value is bad" + case ErrBadExpirationDate: + return "the Expiration header value is bad" + case ErrBadPriority: + return "the apns-priority value is bad" + case ErrBadTopic: + return "the Topic header was invalid" + case ErrBadCertificate: + return "the certificate was bad" + case ErrBadCertificateEnvironment: + return "certificate was for the wrong environment" + case ErrForbidden: + return "there was an error with the certificate" + case ErrMissingTopic: + return "the Topic header of the request was not specified and was required" + case ErrInvalidPushType: + return "the apns-push-type value is invalid" + case ErrTopicDisallowed: + return "pushing to this topic is not allowed" + case ErrUnregistered: + return fmt.Sprintf("device token is inactive for the specified topic (last invalid at %v)", e.Timestamp) + case ErrDeviceTokenNotForTopic: + return "device token does not match the specified topic" + case ErrDuplicateHeaders: + return "one or more headers were repeated" + case ErrBadPath: + return "the request contained a bad :path" + case ErrMethodNotAllowed: + return "the specified :method was not POST" + case ErrIdleTimeout: + return "idle time out" + case ErrShutdown: + return "the server is shutting down" + case ErrInternalServerError: + return "an internal server error occurred" + case ErrServiceUnavailable: + return "the service is unavailable" + default: + return fmt.Sprintf("unknown error: %v", e.Reason.Error()) + } +} diff --git a/vendor/buford/push/header.go b/vendor/buford/push/header.go new file mode 100644 index 0000000..d2e09f8 --- /dev/null +++ b/vendor/buford/push/header.go @@ -0,0 +1,69 @@ +package push + +import ( + "net/http" + "strconv" + "time" +) + +// Headers sent with a push to control the notification (optional) +type Headers struct { + // ID for the notification. Apple generates one if omitted. + // This should be a UUID with 32 lowercase hexadecimal digits. + ID string + + // CollapseID is used to update an existing notification that has the same + // identifier (Notification Management in iOS 10). + CollapseID string + + // Apple will retry delivery until this time. The default behavior only tries once. + Expiration time.Time + + // Allow Apple to group messages together to reduce power consumption. + // By default messages are sent immediately. + LowPriority bool + + // Topic for certificates with multiple topics. + Topic string + + PushType PushType +} + +type PushType string + +const ( + PushTypeAlert PushType = "alert" + PushTypeBackground PushType = "background" +) + +// set headers for an HTTP request +func (h *Headers) set(reqHeader http.Header) { + // headers are optional + if h == nil { + return + } + + if h.ID != "" { + reqHeader.Set("apns-id", h.ID) + } // when omitted, Apple will generate a UUID for you + + if h.CollapseID != "" { + reqHeader.Set("apns-collapse-id", h.CollapseID) + } + + if !h.Expiration.IsZero() { + reqHeader.Set("apns-expiration", strconv.FormatInt(h.Expiration.Unix(), 10)) + } + + if h.LowPriority { + reqHeader.Set("apns-priority", "5") + } // when omitted, the default priority is 10 + + if h.Topic != "" { + reqHeader.Set("apns-topic", h.Topic) + } + + if h.PushType != "" { + reqHeader.Set("apns-push-type", string(h.PushType)) + } +} diff --git a/vendor/buford/push/header_test.go b/vendor/buford/push/header_test.go new file mode 100644 index 0000000..59aa5fb --- /dev/null +++ b/vendor/buford/push/header_test.go @@ -0,0 +1,61 @@ +package push + +import ( + "net/http" + "testing" + "time" +) + +func TestHeaders(t *testing.T) { + headers := Headers{ + ID: "uuid", + CollapseID: "game1.score.identifier", + Expiration: time.Unix(12622780800, 0), + LowPriority: true, + Topic: "bundle-id", + PushType: PushTypeAlert, + } + + reqHeader := http.Header{} + headers.set(reqHeader) + + testHeader(t, reqHeader, "apns-id", "uuid") + testHeader(t, reqHeader, "apns-collapse-id", "game1.score.identifier") + testHeader(t, reqHeader, "apns-expiration", "12622780800") + testHeader(t, reqHeader, "apns-priority", "5") + testHeader(t, reqHeader, "apns-topic", "bundle-id") + testHeader(t, reqHeader, "apns-push-type", "alert") +} + +func TestNilHeader(t *testing.T) { + var headers *Headers + reqHeader := http.Header{} + headers.set(reqHeader) + + testHeader(t, reqHeader, "apns-id", "") + testHeader(t, reqHeader, "apns-collapse-id", "") + testHeader(t, reqHeader, "apns-expiration", "") + testHeader(t, reqHeader, "apns-priority", "") + testHeader(t, reqHeader, "apns-topic", "") + testHeader(t, reqHeader, "apns-push-type", "") +} + +func TestEmptyHeaders(t *testing.T) { + headers := Headers{} + reqHeader := http.Header{} + headers.set(reqHeader) + + testHeader(t, reqHeader, "apns-id", "") + testHeader(t, reqHeader, "apns-collapse-id", "") + testHeader(t, reqHeader, "apns-expiration", "") + testHeader(t, reqHeader, "apns-priority", "") + testHeader(t, reqHeader, "apns-topic", "") + testHeader(t, reqHeader, "apns-push-type", "") +} + +func testHeader(t *testing.T, reqHeader http.Header, key, expected string) { + actual := reqHeader.Get(key) + if actual != expected { + t.Errorf("Expected %s %q, got %q.", key, expected, actual) + } +} diff --git a/vendor/buford/push/queue.go b/vendor/buford/push/queue.go new file mode 100644 index 0000000..2e40dfc --- /dev/null +++ b/vendor/buford/push/queue.go @@ -0,0 +1,64 @@ +package push + +// Queue up notifications without waiting for the response. +type Queue struct { + service *Service + notifications chan notification + Responses chan Response +} + +// notification to send. +type notification struct { + DeviceToken string + Headers *Headers + Payload []byte +} + +// Response from sending a notification. +type Response struct { + DeviceToken string + ID string + Err error +} + +// NewQueue wraps a service with a queue for sending notifications asynchronously. +func NewQueue(service *Service, workers uint) *Queue { + // unbuffered channels + q := &Queue{ + service: service, + notifications: make(chan notification), + Responses: make(chan Response), + } + // startup workers to send notifications + for i := uint(0); i < workers; i++ { + go worker(q) + } + return q +} + +// Push queues a notification to the APN service. +func (q *Queue) Push(deviceToken string, headers *Headers, payload []byte) { + n := notification{ + DeviceToken: deviceToken, + Headers: headers, + Payload: payload, + } + q.notifications <- n +} + +// Close the channels for notifications and Responses and shutdown workers. +// You should only call this after all responses have been received. +func (q *Queue) Close() { + // Stop accepting new notifications and shutdown workers after existing notifications + // are processed: + close(q.notifications) + // Close responses channel to clean up: + close(q.Responses) +} + +func worker(q *Queue) { + for n := range q.notifications { + id, err := q.service.Push(n.DeviceToken, n.Headers, n.Payload) + q.Responses <- Response{DeviceToken: n.DeviceToken, ID: id, Err: err} + } +} diff --git a/vendor/buford/push/queue_test.go b/vendor/buford/push/queue_test.go new file mode 100644 index 0000000..eac2fbc --- /dev/null +++ b/vendor/buford/push/queue_test.go @@ -0,0 +1,52 @@ +package push_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "github.com/RobotsAndPencils/buford/push" +) + +func TestQueuePush(t *testing.T) { + const ( + workers = 10 + number = 100 + ) + payload := []byte(`{ "aps" : { "alert" : "Hello HTTP/2" } }`) + + handler := http.NewServeMux() + server := httptest.NewServer(handler) + + handler.HandleFunc("/3/device/", func(w http.ResponseWriter, r *http.Request) { + deviceToken := strings.TrimPrefix(r.URL.String(), "/3/device/") + // echo back the deviceToken as the id (not the real behavior) + w.Header().Set("apns-id", deviceToken) + }) + + service := push.NewService(http.DefaultClient, server.URL) + queue := push.NewQueue(service, workers) + var wg sync.WaitGroup + + go func() { + for resp := range queue.Responses { + if resp.Err != nil { + t.Error(resp.Err) + } + if resp.ID != resp.DeviceToken { + t.Errorf("Expected %q == %q.", resp.ID, resp.DeviceToken) + } + wg.Done() + } + }() + + for i := 0; i < number; i++ { + wg.Add(1) + queue.Push(fmt.Sprintf("%04d", i), nil, payload) + } + wg.Wait() + queue.Close() +} diff --git a/vendor/buford/push/service.go b/vendor/buford/push/service.go new file mode 100644 index 0000000..d1388e1 --- /dev/null +++ b/vendor/buford/push/service.go @@ -0,0 +1,119 @@ +// Package push sends notifications over HTTP/2 to +// Apple's Push Notification Service. +package push + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/net/http2" +) + +// Apple host locations for configuring Service. +const ( + Development = "https://api.development.push.apple.com" + Development2197 = "https://api.development.push.apple.com:2197" + Production = "https://api.push.apple.com" + Production2197 = "https://api.push.apple.com:2197" +) + +const maxPayload = 4096 // 4KB at most + +// Service is the Apple Push Notification Service that you send notifications to. +type Service struct { + Host string + Client *http.Client +} + +// NewService creates a new service to connect to APN. +func NewService(client *http.Client, host string) *Service { + return &Service{ + Client: client, + Host: host, + } +} + +// NewClient sets up an HTTP/2 client for a certificate. +func NewClient(cert tls.Certificate) (*http.Client, error) { + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + config.BuildNameToCertificate() + transport := &http.Transport{TLSClientConfig: config} + + if err := http2.ConfigureTransport(transport); err != nil { + return nil, err + } + + return &http.Client{Transport: transport}, nil +} + +// Push sends a notification and waits for a response. +func (s *Service) Push(deviceToken string, headers *Headers, payload []byte) (string, error) { + // check payload length before even hitting Apple. + if len(payload) > maxPayload { + return "", &Error{ + Reason: ErrPayloadTooLarge, + Status: http.StatusRequestEntityTooLarge, + } + } + + urlStr := fmt.Sprintf("%v/3/device/%v", s.Host, deviceToken) + + req, err := http.NewRequest("POST", urlStr, bytes.NewReader(payload)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + headers.set(req.Header) + + resp, err := s.Client.Do(req) + + if err != nil { + if e, ok := err.(*url.Error); ok { + if e, ok := e.Err.(http2.GoAwayError); ok { + // parse DebugData as JSON. no status code known (0) + return "", parseErrorResponse(strings.NewReader(e.DebugData), 0) + } + } + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return resp.Header.Get("apns-id"), nil + } + + return "", parseErrorResponse(resp.Body, resp.StatusCode) +} + +func parseErrorResponse(body io.Reader, statusCode int) error { + var response struct { + // Reason for failure + Reason string `json:"reason"` + // Timestamp for 410 StatusGone (ErrUnregistered) + Timestamp int64 `json:"timestamp"` + } + err := json.NewDecoder(body).Decode(&response) + if err != nil { + return err + } + + es := &Error{ + Reason: mapErrorReason(response.Reason), + Status: statusCode, + } + + if response.Timestamp != 0 { + // the response.Timestamp is Milliseconds, but time.Unix() requires seconds + es.Timestamp = time.Unix(response.Timestamp/1000, 0).UTC() + } + return es +} diff --git a/vendor/buford/push/service_test.go b/vendor/buford/push/service_test.go new file mode 100644 index 0000000..d08a666 --- /dev/null +++ b/vendor/buford/push/service_test.go @@ -0,0 +1,165 @@ +package push_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + "time" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/push" +) + +func TestNewClient(t *testing.T) { + const name = "../testdata/cert.p12" + + cert, err := certificate.Load(name, "") + if err != nil { + t.Fatal(err) + } + + _, err = push.NewClient(cert) + if err != nil { + t.Fatal(err) + } +} + +func TestPush(t *testing.T) { + deviceToken := "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433" + payload := []byte(`{ "aps" : { "alert" : "Hello HTTP/2" } }`) + apnsID := "922D9F1F-B82E-B337-EDC9-DB4FC8527676" + + handler := http.NewServeMux() + server := httptest.NewServer(handler) + + handler.HandleFunc("/3/device/", func(w http.ResponseWriter, r *http.Request) { + expectURL := fmt.Sprintf("/3/device/%s", deviceToken) + if r.URL.String() != expectURL { + t.Errorf("Expected url %v, got %v", expectURL, r.URL) + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(body, payload) { + t.Errorf("Expected body %v, got %v", payload, body) + } + + w.Header().Set("apns-id", apnsID) + }) + + service := push.NewService(http.DefaultClient, server.URL) + + id, err := service.Push(deviceToken, &push.Headers{}, payload) + if err != nil { + t.Error(err) + } + if id != apnsID { + t.Errorf("Expected apns-id %q, but got %q.", apnsID, id) + } +} + +func TestBadPriorityPush(t *testing.T) { + deviceToken := "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433" + payload := []byte(`{ "aps" : { "alert" : "Hello HTTP/2" } }`) + + handler := http.NewServeMux() + server := httptest.NewServer(handler) + + handler.HandleFunc("/3/device/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"reason": "BadPriority"}`)) + }) + + service := push.NewService(http.DefaultClient, server.URL) + + _, err := service.Push(deviceToken, nil, payload) + + e, ok := err.(*push.Error) + if !ok { + t.Fatalf("Expected push error, got %v.", err) + } + + if e.Reason != push.ErrBadPriority { + t.Errorf("Expected error %v, got %v.", push.ErrBadPriority, err) + } + + const expectedMessage = "the apns-priority value is bad" + if e.Error() != expectedMessage { + t.Errorf("Expected error message %q, got %q.", expectedMessage, e.Error()) + } + + if e.Status != http.StatusBadRequest { + t.Errorf("Expected status %v, got %v.", http.StatusBadRequest, e.Status) + } + + if !e.Timestamp.IsZero() { + t.Errorf("Expected zero timestamp, got %v.", e.Timestamp) + } +} + +func TestTimestampError(t *testing.T) { + deviceToken := "c2732227a1d8021cfaf781d71fb2f908c61f5861079a00954a5453f1d0281433" + payload := []byte(`{ "aps" : { "alert" : "Hello HTTP/2" } }`) + + handler := http.NewServeMux() + server := httptest.NewServer(handler) + + handler.HandleFunc("/3/device/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusGone) + w.Write([]byte(`{"reason":"Unregistered","timestamp":12622780800000}`)) + }) + + service := push.NewService(http.DefaultClient, server.URL) + + _, err := service.Push(deviceToken, nil, payload) + + e, ok := err.(*push.Error) + if !ok { + t.Fatalf("Expected push error, got %v.", err) + } + + if e.Reason != push.ErrUnregistered { + t.Errorf("Expected error reason %v, got %v.", push.ErrUnregistered, err) + } + + const expectedMessage = "device token is inactive for the specified topic (last invalid at 2370-01-01 00:00:00 +0000 UTC)" + if e.Error() != expectedMessage { + t.Errorf("Expected error message %q, got %q.", expectedMessage, e.Error()) + } + + if e.Status != http.StatusGone { + t.Errorf("Expected status %v, got %v.", http.StatusGone, e.Status) + } + + expected := time.Unix(12622780800, 0).UTC() + if e.Timestamp != expected { + t.Errorf("Expected timestamp %v, got %v.", expected, e.Timestamp) + } +} + +func TestPayloadTooLarge(t *testing.T) { + payload := []byte(strings.Repeat("0123456789abcdef", 256) + "x") + + service := push.NewService(http.DefaultClient, "host") + _, err := service.Push("device-token", nil, payload) + if err == nil { + t.Fatal("Expected error, got none") + } + if _, ok := err.(*push.Error); !ok { + t.Fatalf("Expected push error, got %v.", err) + } + + e := err.(*push.Error) + if e.Reason != push.ErrPayloadTooLarge { + t.Errorf("Expected PayloadTooLarge, got reason %q.", e.Reason) + } + if e.Status != http.StatusRequestEntityTooLarge { + t.Errorf("Expected status %v, got %v.", http.StatusRequestEntityTooLarge, e.Status) + } +} diff --git a/vendor/buford/pushpackage/checksum.go b/vendor/buford/pushpackage/checksum.go new file mode 100644 index 0000000..e89a664 --- /dev/null +++ b/vendor/buford/pushpackage/checksum.go @@ -0,0 +1,17 @@ +package pushpackage + +import ( + "crypto/sha1" + "encoding/hex" + "io" +) + +// copyAndChecksum calculates a checksum while writing to another output +func copyAndChecksum(w io.Writer, r io.Reader) (string, error) { + h := sha1.New() + mw := io.MultiWriter(w, h) + if _, err := io.Copy(mw, r); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/vendor/buford/pushpackage/checksum_test.go b/vendor/buford/pushpackage/checksum_test.go new file mode 100644 index 0000000..567af8d --- /dev/null +++ b/vendor/buford/pushpackage/checksum_test.go @@ -0,0 +1,27 @@ +package pushpackage + +import ( + "bytes" + "strings" + "testing" +) + +func TestChecksum(t *testing.T) { + content := `{"websiteName": "Bay Airlines"}` + r := strings.NewReader(content) + + // confirmed matches openssl sha1 + expected := "82c436ae8f6702859cd4dd4c5461c71b77a586a3" + + buf := new(bytes.Buffer) + c, err := copyAndChecksum(buf, r) + if err != nil { + t.Fatal(err) + } + if c != expected { + t.Errorf("Expected checksum %q, got %q", expected, c) + } + if buf.String() != content { + t.Errorf("Expected content %q, got %q", content, buf.String()) + } +} diff --git a/vendor/buford/pushpackage/pushpackage.go b/vendor/buford/pushpackage/pushpackage.go new file mode 100644 index 0000000..64ba9ae --- /dev/null +++ b/vendor/buford/pushpackage/pushpackage.go @@ -0,0 +1,132 @@ +// Package pushpackage creates website push packages and wallet pass packages. +package pushpackage + +import ( + "archive/zip" + "bytes" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "io" + "os" + + "github.com/aai/gocrypto/pkcs7" +) + +// Package for website push package or wallet pass package. +type Package struct { + z *zip.Writer + + // manifest is a map of relative file paths to their SHA checksums + manifest map[string]string + + err error +} + +// New push package will be written to w. +func New(w io.Writer) Package { + return Package{ + z: zip.NewWriter(w), + manifest: make(map[string]string), + } +} + +// EncodeJSON to the push package. +func (p *Package) EncodeJSON(name string, e interface{}) { + if p.err != nil { + return + } + + b, err := json.Marshal(e) + if err != nil { + p.err = err + return + } + r := bytes.NewReader(b) + + p.Copy(name, r) +} + +// Copy reader to the push package. +func (p *Package) Copy(name string, r io.Reader) { + if p.err != nil { + return + } + + zf, err := p.z.Create(name) + if err != nil { + p.err = err + return + } + + checksum, err := copyAndChecksum(zf, r) + if err != nil { + p.err = err + return + } + + p.manifest[name] = checksum +} + +// File writes a file to the push package. +// +// NOTE: Name is a relative path. Only forward slashes are allowed. +func (p *Package) File(name, src string) { + if p.err != nil { + return + } + + f, err := os.Open(src) + if err != nil { + p.err = err + return + } + defer f.Close() + p.Copy(name, f) +} + +// Sign the package and close. +// Passbook needs Apple's intermediate WWDR certificate. +func (p *Package) Sign(cert tls.Certificate, wwdr *x509.Certificate) error { + if p.err != nil { + return p.err + } + + // assert that private key is RSA + key, ok := cert.PrivateKey.(*rsa.PrivateKey) + if !ok { + return errors.New("expected RSA private key type") + } + + manifestBytes, err := json.Marshal(p.manifest) + if err != nil { + return err + } + + zf, err := p.z.Create("manifest.json") + if err != nil { + return err + } + zf.Write(manifestBytes) + + // sign manifest.json with PKCS #7 + // and add signature to the zip file + zf, err = p.z.Create("signature") + if err != nil { + return err + } + sig, err := pkcs7.Sign2(bytes.NewReader(manifestBytes), cert.Leaf, key, wwdr) + if err != nil { + return err + } + zf.Write(sig) + + return p.z.Close() +} + +// Error that occurred while adding files to the push package. +func (p *Package) Error() error { + return p.err +} diff --git a/vendor/buford/pushpackage/pushpackage_test.go b/vendor/buford/pushpackage/pushpackage_test.go new file mode 100644 index 0000000..9472ee4 --- /dev/null +++ b/vendor/buford/pushpackage/pushpackage_test.go @@ -0,0 +1,65 @@ +package pushpackage_test + +import ( + "archive/zip" + "bytes" + "io/ioutil" + "testing" + + "github.com/RobotsAndPencils/buford/certificate" + "github.com/RobotsAndPencils/buford/pushpackage" +) + +func TestNew(t *testing.T) { + website := pushpackage.Website{ + Name: "Bay Airlines", + PushID: "web.com.example.domain", + AllowedDomains: []string{"http://domain.example.com"}, + URLFormatString: `http://domain.example.com/%@/?flight=%@`, + AuthenticationToken: "19f8d7a6e9fb8a7f6d9330dabe", + WebServiceURL: "https://example.com/push", + } + + cert, err := certificate.Load("../testdata/cert.p12", "") + if err != nil { + t.Fatal(err) + } + + buf := new(bytes.Buffer) + + pkg := pushpackage.New(buf) + pkg.EncodeJSON("website.json", website) + pkg.File("icon.iconset/icon_128x128@2x.png", "../testdata/gopher.png") + if err := pkg.Sign(cert, nil); err != nil { + t.Fatal(err) + } + + expected := map[string]string{ + "website.json": `{"websiteName":"Bay Airlines","websitePushID":"web.com.example.domain","allowedDomains":["http://domain.example.com"],"urlFormatString":"http://domain.example.com/%@/?flight=%@","authenticationToken":"19f8d7a6e9fb8a7f6d9330dabe","webServiceURL":"https://example.com/push"}`, + "manifest.json": `{"icon.iconset/icon_128x128@2x.png":"5d31b7d2ea66ec7087c3789b2c6ca2aad67e459c","website.json":"8225d6cdd71f00888ff576aaab8d7ec4a27553c7"}`, + } + + z, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + for _, f := range z.File { + if exp, ok := expected[f.Name]; ok { + b, err := zipReadFile(f) + if err != nil { + t.Fatal(err) + } + if string(b) != exp { + t.Errorf("Unexpected content for %s: %s", f.Name, b) + } + } else { + t.Log(f.Name) + } + } +} + +func zipReadFile(f *zip.File) ([]byte, error) { + zf, err := f.Open() + if err != nil { + return nil, err + } + defer zf.Close() + return ioutil.ReadAll(zf) +} diff --git a/vendor/buford/pushpackage/website.go b/vendor/buford/pushpackage/website.go new file mode 100644 index 0000000..d569c82 --- /dev/null +++ b/vendor/buford/pushpackage/website.go @@ -0,0 +1,23 @@ +package pushpackage + +// Website JSON for creating a push package. +type Website struct { + // Website Name shown in the Notification Center. + Name string `json:"websiteName"` + + // Website Push ID (eg. web.com.domain) + PushID string `json:"websitePushID"` + + // Websites that can request permission from the user. + AllowedDomains []string `json:"allowedDomains"` + + // http(s) URL for clicked notifications with %@ placeholders. + URLFormatString string `json:"urlFormatString"` + + // A 16+ character string to identify the user. + AuthenticationToken string `json:"authenticationToken"` + + // Location of your web service. Must be HTTPS. + // Don't include a trailing slash. + WebServiceURL string `json:"webServiceURL"` +} diff --git a/vendor/buford/test.sh b/vendor/buford/test.sh new file mode 100755 index 0000000..188b3cc --- /dev/null +++ b/vendor/buford/test.sh @@ -0,0 +1,10 @@ +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v '/certificate'); do + go test -v -race -coverprofile=profile.out -covermode=atomic $d + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/vendor/buford/testdata/README.md b/vendor/buford/testdata/README.md new file mode 100644 index 0000000..c7d5a9d --- /dev/null +++ b/vendor/buford/testdata/README.md @@ -0,0 +1,8 @@ + +Self-signed cert with OpenSSL + +``` +/usr/local/opt/openssl/bin/openssl req -x509 -newkey rsa:2048 -out cert-self.pem -keyout key-self.pem -days 365 -nodes + +/usr/local/opt/openssl/bin/openssl pkcs12 -in cert-self.pem -inkey key-self.pem -out cert-self.p12 -export +``` diff --git a/vendor/buford/testdata/cert.p12 b/vendor/buford/testdata/cert.p12 new file mode 100644 index 0000000..53069e2 Binary files /dev/null and b/vendor/buford/testdata/cert.p12 differ diff --git a/vendor/buford/testdata/gopher.png b/vendor/buford/testdata/gopher.png new file mode 100644 index 0000000..6fadb36 Binary files /dev/null and b/vendor/buford/testdata/gopher.png differ