This repository has been archived by the owner on Jun 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathservice.go
119 lines (100 loc) · 2.85 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
}