Skip to content

Commit 2bce391

Browse files
author
Дмитрий Седых
committed
v3.1
1 parent 78f97d3 commit 2bce391

14 files changed

+911
-213
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
*.tar
44
*.out
55
push/push
6+
7+
*.p8

certificate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type CertificateInfo struct {
4848
}
4949

5050
// GetCertificateInfo parses and returns information about the certificate.
51-
func GetCertificateInfo(certificate tls.Certificate) *CertificateInfo {
51+
func GetCertificateInfo(certificate *tls.Certificate) *CertificateInfo {
5252
var cert = certificate.Leaf
5353
if cert == nil {
5454
var err error

certificate_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func TestCertificate(t *testing.T) {
3030
fmt.Println("Certificate error:", err)
3131
}
3232
certificate.Leaf = nil
33-
info := GetCertificateInfo(*certificate)
33+
info := GetCertificateInfo(certificate)
3434
if info == nil {
3535
t.Error("Bad certificate info")
3636
continue
@@ -85,7 +85,7 @@ func TestCertificateWithErrors(t *testing.T) {
8585
PrivateKey: nil,
8686
Leaf: nil,
8787
}
88-
info := GetCertificateInfo(*certificate)
88+
info := GetCertificateInfo(certificate)
8989
if info != nil {
9090
t.Error("Bad info")
9191
}
@@ -94,7 +94,7 @@ func TestCertificateWithErrors(t *testing.T) {
9494
fmt.Println("Certificate error:", err)
9595
}
9696
certificate.Leaf = nil
97-
info := GetCertificateInfo(*certificate)
97+
info := GetCertificateInfo(certificate)
9898
if info == nil {
9999
t.Error("Bad certificate info")
100100
continue

certificates.tar.enc

2.5 KB
Binary file not shown.

client.go

Lines changed: 105 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,183 +1,144 @@
11
package apns
22

33
import (
4-
"bytes"
54
"crypto/tls"
6-
"encoding/hex"
7-
"encoding/json"
8-
"fmt"
95
"net/http"
106
"net/url"
11-
"strconv"
127
"strings"
138
"time"
149

1510
"golang.org/x/net/http2"
1611
)
1712

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.
1943
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
2348
}
2449

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}
3066
if err := http2.ConfigureTransport(transport); err != nil {
3167
panic(err) // HTTP/2 initialization error
3268
}
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,
3972
}
73+
return client
4074
}
4175

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+
}
9179

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)
9584
}
9685

97-
// Push sends a notification to the Apple server.
86+
// Push send push notification to APNS API.
9887
//
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)
141105
if err != nil {
142106
return "", err
143107
}
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)
154113
}
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())
157121
}
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))
161131
}
162-
req.Header.Set("apns-topic", n.Topic)
163132
}
164-
resp, err := c.httpСlient.Do(req)
165133
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-
}
171134
return "", err
172135
}
136+
// For a successful request, the body of the response is empty. On failure,
137+
// the response body contains a JSON dictionary.
173138
defer resp.Body.Close()
174-
// defer func() {
175-
// io.CopyN(ioutil.Discard, resp.Body, 2<<10)
176-
// resp.Body.Close()
177-
// }()
178139
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
181142
}
182-
return id, nil
143+
return id, parseError(resp.StatusCode, resp.Body)
183144
}

client_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ func TestClient2(t *testing.T) {
6868
if err == nil {
6969
t.Error("bad token size")
7070
}
71-
client.Sandbox = true
7271
_, err = client.Push(Notification{
7372
ID: "123e4567-e89b-12d3-a456-42665544000",
7473
Expiration: time.Now().Add(time.Hour),

0 commit comments

Comments
 (0)