-
Notifications
You must be signed in to change notification settings - Fork 248
/
validator.go
216 lines (189 loc) · 6.55 KB
/
validator.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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//go:generate mockgen -destination=mocks/appstore.go -package=mocks github.com/awa/go-iap/appstore IAPClient
package appstore
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/golang-jwt/jwt/v4"
)
const (
// SandboxURL is the endpoint for sandbox environment.
SandboxURL string = "https://sandbox.itunes.apple.com/verifyReceipt"
// ProductionURL is the endpoint for production environment.
ProductionURL string = "https://buy.itunes.apple.com/verifyReceipt"
// ContentType is the request content-type for apple store.
ContentType string = "application/json; charset=utf-8"
)
// IAPClient is an interface to call validation API in App Store
type IAPClient interface {
Verify(ctx context.Context, reqBody IAPRequest, resp interface{}) error
VerifyWithStatus(ctx context.Context, reqBody IAPRequest, resp interface{}) (int, error)
ParseNotificationV2(tokenStr string, result *jwt.Token) error
ParseNotificationV2WithClaim(tokenStr string, result jwt.Claims) error
}
// Client implements IAPClient
type Client struct {
ProductionURL string
SandboxURL string
httpCli *http.Client
}
// list of errore
var (
ErrAppStoreServer = errors.New("AppStore server error")
ErrInvalidJSON = errors.New("The App Store could not read the JSON object you provided.")
ErrInvalidReceiptData = errors.New("The data in the receipt-data property was malformed or missing.")
ErrReceiptUnauthenticated = errors.New("The receipt could not be authenticated.")
ErrInvalidSharedSecret = errors.New("The shared secret you provided does not match the shared secret on file for your account.")
ErrServerUnavailable = errors.New("The receipt server is not currently available.")
ErrReceiptIsForTest = errors.New("This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.")
ErrReceiptIsForProduction = errors.New("This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.")
ErrReceiptUnauthorized = errors.New("This receipt could not be authorized. Treat this the same as if a purchase was never made.")
ErrInternalDataAccessError = errors.New("Internal data access error.")
ErrUnknown = errors.New("An unknown error occurred")
)
// HandleError returns error message by status code
func HandleError(status int) error {
var e error
switch status {
case 0:
return nil
case 21000:
e = ErrInvalidJSON
case 21002:
e = ErrInvalidReceiptData
case 21003:
e = ErrReceiptUnauthenticated
case 21004:
e = ErrInvalidSharedSecret
case 21005:
e = ErrServerUnavailable
case 21007:
e = ErrReceiptIsForTest
case 21008:
e = ErrReceiptIsForProduction
case 21009:
e = ErrInternalDataAccessError
case 21010:
e = ErrReceiptUnauthorized
default:
if status >= 21100 && status <= 21199 {
e = ErrInternalDataAccessError
} else {
e = ErrUnknown
}
}
return fmt.Errorf("status %d: %w", status, e)
}
// New creates a client object
func New() *Client {
client := &Client{
ProductionURL: ProductionURL,
SandboxURL: SandboxURL,
httpCli: &http.Client{
Timeout: 10 * time.Second,
},
}
return client
}
// NewWithClient creates a client with a custom http client.
func NewWithClient(client *http.Client) *Client {
return &Client{
ProductionURL: ProductionURL,
SandboxURL: SandboxURL,
httpCli: client,
}
}
// Verify sends receipts and gets validation result
func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interface{}) error {
_, err := c.verify(ctx, reqBody, result)
return err
}
// VerifyWithStatus sends receipts and gets validation result with status code
// If the Apple verification receipt server is unhealthy and responds with an HTTP status code in the 5xx range, that status code will be returned.
func (c *Client) VerifyWithStatus(ctx context.Context, reqBody IAPRequest, result interface{}) (int, error) {
return c.verify(ctx, reqBody, result)
}
func (c *Client) verify(ctx context.Context, reqBody IAPRequest, result interface{}) (int, error) {
b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(reqBody); err != nil {
return 0, err
}
req, err := http.NewRequest("POST", c.ProductionURL, b)
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", ContentType)
req = req.WithContext(ctx)
resp, err := c.httpCli.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return resp.StatusCode, fmt.Errorf("Received http status code %d from the App Store: %w", resp.StatusCode, ErrAppStoreServer)
}
return c.parseResponse(resp, result, ctx, reqBody)
}
func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx context.Context, reqBody IAPRequest) (int, error) {
// Read the body now so that we can unmarshal it twice
buf, err := io.ReadAll(resp.Body)
if err != nil {
return 0, err
}
err = json.Unmarshal(buf, &result)
if err != nil {
return 0, err
}
// https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL
var r StatusResponse
err = json.Unmarshal(buf, &r)
if err != nil {
return 0, err
}
if r.Status == 21007 {
b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(reqBody); err != nil {
return 0, err
}
req, err := http.NewRequest("POST", c.SandboxURL, b)
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", ContentType)
req = req.WithContext(ctx)
resp, err := c.httpCli.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return resp.StatusCode, fmt.Errorf("Received http status code %d from the App Store Sandbox: %w", resp.StatusCode, ErrAppStoreServer)
}
return r.Status, json.NewDecoder(resp.Body).Decode(result)
}
return r.Status, nil
}
// ParseNotificationV2 parse notification from App Store Server
func (c *Client) ParseNotificationV2(tokenStr string, result *jwt.Token) error {
cert := Cert{}
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return cert.ExtractPublicKeyFromToken(tokenStr)
})
if token != nil {
*result = *token
}
return err
}
// ParseNotificationV2WithClaim parse notification from App Store Server
func (c *Client) ParseNotificationV2WithClaim(tokenStr string, result jwt.Claims) error {
cert := Cert{}
_, err := jwt.ParseWithClaims(tokenStr, result, func(token *jwt.Token) (interface{}, error) {
return cert.ExtractPublicKeyFromToken(tokenStr)
})
return err
}