|
1 | 1 | package apns
|
2 | 2 |
|
3 | 3 | import (
|
4 |
| - "bytes" |
5 | 4 | "crypto/tls"
|
6 |
| - "encoding/hex" |
7 |
| - "encoding/json" |
8 |
| - "fmt" |
9 | 5 | "net/http"
|
10 | 6 | "net/url"
|
11 |
| - "strconv" |
12 | 7 | "strings"
|
13 | 8 | "time"
|
14 | 9 |
|
15 | 10 | "golang.org/x/net/http2"
|
16 | 11 | )
|
17 | 12 |
|
18 |
| -// Client describes APNS client service to send notifications to devices. |
| 13 | +// Timeout contains the maximum waiting time connection to the APNS server. |
| 14 | +var Timeout = 15 * time.Second |
| 15 | + |
| 16 | +// Client supports APNs Provider API. |
| 17 | +// |
| 18 | +// The APNs provider API lets you send remote notifications to your app on iOS, |
| 19 | +// tvOS, and macOS devices, and to Apple Watch via iOS. The API is based on the |
| 20 | +// HTTP/2 network protocol. Each interaction starts with a POST request, |
| 21 | +// containing a JSON payload, that you send from your provider server to APNs. |
| 22 | +// APNs then forwards the notification to your app on a specific user device. |
| 23 | +// |
| 24 | +// The first step in sending a remote notification is to establish a connection |
| 25 | +// with the appropriate APNs server Host: |
| 26 | +// Development server: api.development.push.apple.com:443 |
| 27 | +// Production server: api.push.apple.com:443 |
| 28 | +// |
| 29 | +// Note: You can alternatively use port 2197 when communicating with APNs. You |
| 30 | +// might do this, for example, to allow APNs traffic through your firewall but |
| 31 | +// to block other HTTPS traffic. |
| 32 | +// |
| 33 | +// The APNs server allows multiple concurrent streams for each connection. The |
| 34 | +// exact number of streams is based on the authentication method used (i.e. |
| 35 | +// provider certificate or token) and the server load, so do not assume a |
| 36 | +// specific number of streams. When you connect to APNs without a provider |
| 37 | +// certificate, only one stream is allowed on the connection until you send a |
| 38 | +// push message with valid token. |
| 39 | +// |
| 40 | +// It is recommended to close all existing connections to APNs and open new |
| 41 | +// connections when existing certificate or the key used to sign provider tokens |
| 42 | +// is revoked. |
19 | 43 | type Client struct {
|
20 |
| - *CertificateInfo // certificate info |
21 |
| - Sandbox bool // sandbox flag |
22 |
| - httpСlient *http.Client // http client for push |
| 44 | + Host string // http URL |
| 45 | + ci *CertificateInfo // certificate |
| 46 | + token *ProviderToken // provider token |
| 47 | + httpСlient *http.Client // http client for push |
23 | 48 | }
|
24 | 49 |
|
25 |
| -// New initialize and return the APNS client to send notifications. |
26 |
| -func New(certificate tls.Certificate) *Client { |
27 |
| - var tlsConfig = &tls.Config{ |
28 |
| - Certificates: []tls.Certificate{certificate}} |
29 |
| - var transport = &http.Transport{TLSClientConfig: tlsConfig} |
| 50 | +func newClient(certificate *tls.Certificate, pt *ProviderToken) *Client { |
| 51 | + client := &Client{ |
| 52 | + Host: "https://api.push.apple.com", |
| 53 | + } |
| 54 | + if pt != nil { |
| 55 | + client.token = pt |
| 56 | + } |
| 57 | + tlsConfig := new(tls.Config) |
| 58 | + if certificate != nil { |
| 59 | + tlsConfig.Certificates = []tls.Certificate{*certificate} |
| 60 | + client.ci = GetCertificateInfo(certificate) |
| 61 | + if !client.ci.Production { |
| 62 | + client.Host = "https://api.development.push.apple.com" |
| 63 | + } |
| 64 | + } |
| 65 | + transport := &http.Transport{TLSClientConfig: tlsConfig} |
30 | 66 | if err := http2.ConfigureTransport(transport); err != nil {
|
31 | 67 | panic(err) // HTTP/2 initialization error
|
32 | 68 | }
|
33 |
| - return &Client{ |
34 |
| - CertificateInfo: GetCertificateInfo(certificate), |
35 |
| - httpСlient: &http.Client{ |
36 |
| - Timeout: 15 * time.Second, |
37 |
| - Transport: transport, |
38 |
| - }, |
| 69 | + client.httpСlient = &http.Client{ |
| 70 | + Timeout: Timeout, |
| 71 | + Transport: transport, |
39 | 72 | }
|
| 73 | + return client |
40 | 74 | }
|
41 | 75 |
|
42 |
| -// Notification describes the information for sending Apple Push. |
43 |
| -type Notification struct { |
44 |
| - // Unique device token for the app. |
45 |
| - // |
46 |
| - // Every notification that your provider sends to APNs must be accompanied |
47 |
| - // by the device token associated of the device for which the notification |
48 |
| - // is intended. |
49 |
| - Token string |
50 |
| - |
51 |
| - // A canonical UUID that identifies the notification. If there is an error |
52 |
| - // sending the notification, APNs uses this value to identify the |
53 |
| - // notification to your server. |
54 |
| - // |
55 |
| - // The canonical form is 32 lowercase hexadecimal digits, displayed in five |
56 |
| - // groups separated by hyphens in the form 8-4-4-4-12. An example UUID is |
57 |
| - // as follows: 123e4567-e89b-12d3-a456-42665544000 |
58 |
| - // |
59 |
| - // If you omit this header, a new UUID is created by APNs and returned in |
60 |
| - // the response. |
61 |
| - ID string |
62 |
| - |
63 |
| - // This identifies the date when the notification is no longer valid and |
64 |
| - // can be discarded. |
65 |
| - // |
66 |
| - // If this value is in future time, APNs stores the notification and tries |
67 |
| - // to deliver it at least once, repeating the attempt as needed if it is |
68 |
| - // unable to deliver the notification the first time. If the value is |
69 |
| - // before now, APNs treats the notification as if it expires immediately |
70 |
| - // and does not store the notification or attempt to redeliver it. |
71 |
| - Expiration time.Time |
72 |
| - |
73 |
| - // Specify the hexadecimal bytes (hex-string) of the device token for the |
74 |
| - // target device. |
75 |
| - // |
76 |
| - // Flag for send the push message at a time that takes into account power |
77 |
| - // considerations for the device. Notifications with this priority might be |
78 |
| - // grouped and delivered in bursts. They are throttled, and in some cases |
79 |
| - // are not delivered. |
80 |
| - LowPriority bool |
81 |
| - |
82 |
| - // The topic of the remote notification, which is typically the bundle ID |
83 |
| - // for your app. The certificate you create in Member Center must include |
84 |
| - // the capability for this topic. |
85 |
| - // |
86 |
| - // If your certificate includes multiple topics, you can specify a value |
87 |
| - // for this. If you omit this or your APNs certificate does not specify |
88 |
| - // multiple topics, the APNs server uses the certificate’s Subject as the |
89 |
| - // default topic. |
90 |
| - Topic string |
| 76 | +func New(certificate tls.Certificate) *Client { |
| 77 | + return newClient(&certificate, nil) |
| 78 | +} |
91 | 79 |
|
92 |
| - // The body content of your message is the JSON dictionary object |
93 |
| - // containing the notification data. |
94 |
| - Payload interface{} |
| 80 | +// NewWithToken returns an initialized Client with JSON Web Token (JWT) |
| 81 | +// authentication support. |
| 82 | +func NewWithToken(pt *ProviderToken) *Client { |
| 83 | + return newClient(nil, pt) |
95 | 84 | }
|
96 | 85 |
|
97 |
| -// Push sends a notification to the Apple server. |
| 86 | +// Push send push notification to APNS API. |
98 | 87 | //
|
99 |
| -// Return the ID value from the notification. If no value was included in the |
100 |
| -// notification, the server creates a new UUID and returns it. |
101 |
| -func (c *Client) Push(n Notification) (id string, err error) { |
102 |
| - var payload []byte |
103 |
| - switch data := n.Payload.(type) { |
104 |
| - case []byte: |
105 |
| - payload = data |
106 |
| - case string: |
107 |
| - payload = []byte(data) |
108 |
| - case json.RawMessage: |
109 |
| - payload = []byte(data) |
110 |
| - default: |
111 |
| - if payload, err = json.Marshal(n.Payload); err != nil { |
112 |
| - return "", err |
113 |
| - } |
114 |
| - } |
115 |
| - if len(payload) > 4096 { |
116 |
| - return "", &Error{ |
117 |
| - Status: http.StatusRequestEntityTooLarge, |
118 |
| - Reason: "PayloadTooLarge", |
119 |
| - } |
120 |
| - } |
121 |
| - // check token format and length |
122 |
| - if l := len(n.Token); l < 64 || l > 200 { |
123 |
| - return "", &Error{ |
124 |
| - Status: http.StatusBadRequest, |
125 |
| - Reason: "BadDeviceToken", |
126 |
| - } |
127 |
| - } |
128 |
| - if _, err = hex.DecodeString(n.Token); err != nil { |
129 |
| - return "", &Error{ |
130 |
| - Status: http.StatusBadRequest, |
131 |
| - Reason: "BadDeviceToken", |
132 |
| - } |
133 |
| - } |
134 |
| - var host = "https://api.push.apple.com" |
135 |
| - if !c.CertificateInfo.Production || |
136 |
| - (c.Sandbox && c.CertificateInfo.Development) { |
137 |
| - host = "https://api.development.push.apple.com" |
138 |
| - } |
139 |
| - req, err := http.NewRequest(http.MethodPost, |
140 |
| - fmt.Sprintf("%v/3/device/%v", host, n.Token), bytes.NewReader(payload)) |
| 88 | +// The APNs Provider API consists of a request and a response that you configure |
| 89 | +// and send using an HTTP/2 POST command. You use the request to send a push |
| 90 | +// notification to the APNs server and use the response to determine the results |
| 91 | +// of that request. |
| 92 | +// |
| 93 | +// Response from APNs: |
| 94 | +// - The apns-id value from the request. If no value was included in the |
| 95 | +// request, the server creates a new UUID and returns it in this header. |
| 96 | +// - :status - the HTTP status code. |
| 97 | +// - reason - the error indicating the reason for the failure. The error code |
| 98 | +// is specified as a string. |
| 99 | +// - timestamp - if the value in the :status header is 410, the value of this |
| 100 | +// key is the last time at which APNs confirmed that the device token was no |
| 101 | +// longer valid for the topic. Stop pushing notifications until the device |
| 102 | +// registers a token with a later timestamp with your provider. |
| 103 | +func (c *Client) Push(notification Notification) (id string, err error) { |
| 104 | + req, err := notification.request(c.Host) |
141 | 105 | if err != nil {
|
142 | 106 | return "", err
|
143 | 107 | }
|
144 |
| - req.Header.Set("Content-Type", "application/json") |
145 |
| - if n.ID != "" { |
146 |
| - req.Header.Set("apns-id", n.ID) |
147 |
| - } |
148 |
| - if !n.Expiration.IsZero() { |
149 |
| - var exp string = "0" |
150 |
| - if !n.Expiration.Before(time.Now()) { |
151 |
| - exp = strconv.FormatInt(n.Expiration.Unix(), 10) |
152 |
| - } |
153 |
| - req.Header.Set("apns-expiration", exp) |
| 108 | + // add default certificate topic |
| 109 | + if notification.Topic == "" && c.ci != nil && len(c.ci.Topics) > 0 { |
| 110 | + // If your certificate includes multiple topics, you must specify a |
| 111 | + // value for this header. |
| 112 | + req.Header.Set("apns-topic", c.ci.BundleID) |
154 | 113 | }
|
155 |
| - if n.LowPriority { |
156 |
| - req.Header.Set("apns-priority", "5") |
| 114 | + if c.token != nil { |
| 115 | + // The provider token that authorizes APNs to send push notifications |
| 116 | + // for the specified topics. The token is in Base64URL-encoded JWT |
| 117 | + // format, specified as bearer <provider token>. |
| 118 | + // When the provider certificate is used to establish a connection, this |
| 119 | + // request header is ignored. |
| 120 | + req.Header.Set("authorization", "bearer "+c.token.JWT()) |
157 | 121 | }
|
158 |
| - if len(c.CertificateInfo.Topics) > 0 { |
159 |
| - if n.Topic == "" { |
160 |
| - n.Topic = c.CertificateInfo.BundleID |
| 122 | + |
| 123 | + resp, err := c.httpСlient.Do(req) |
| 124 | + if err, ok := err.(*url.Error); ok { |
| 125 | + // If APNs decides to terminate an established HTTP/2 connection, it |
| 126 | + // sends a GOAWAY frame. The GOAWAY frame includes JSON data in its |
| 127 | + // payload with a reason key, whose value indicates the reason for the |
| 128 | + // connection termination. |
| 129 | + if err, ok := err.Err.(http2.GoAwayError); ok { |
| 130 | + return "", parseError(0, strings.NewReader(err.DebugData)) |
161 | 131 | }
|
162 |
| - req.Header.Set("apns-topic", n.Topic) |
163 | 132 | }
|
164 |
| - resp, err := c.httpСlient.Do(req) |
165 | 133 | if err != nil {
|
166 |
| - if err, ok := err.(*url.Error); ok { |
167 |
| - if err, ok := err.Err.(http2.GoAwayError); ok { |
168 |
| - return "", decodeError(0, strings.NewReader(err.DebugData)) |
169 |
| - } |
170 |
| - } |
171 | 134 | return "", err
|
172 | 135 | }
|
| 136 | + // For a successful request, the body of the response is empty. On failure, |
| 137 | + // the response body contains a JSON dictionary. |
173 | 138 | defer resp.Body.Close()
|
174 |
| - // defer func() { |
175 |
| - // io.CopyN(ioutil.Discard, resp.Body, 2<<10) |
176 |
| - // resp.Body.Close() |
177 |
| - // }() |
178 | 139 | id = resp.Header.Get("apns-id")
|
179 |
| - if resp.StatusCode != http.StatusOK { |
180 |
| - return id, decodeError(resp.StatusCode, resp.Body) |
| 140 | + if resp.StatusCode == http.StatusOK { |
| 141 | + return id, nil |
181 | 142 | }
|
182 |
| - return id, nil |
| 143 | + return id, parseError(resp.StatusCode, resp.Body) |
183 | 144 | }
|
0 commit comments