1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package storage
16
17 import (
18 "crypto"
19 "crypto/rand"
20 "crypto/rsa"
21 "crypto/sha256"
22 "encoding/base64"
23 "encoding/json"
24 "errors"
25 "fmt"
26 "net/url"
27 "strings"
28 "time"
29 )
30
31
32
33
34 type PostPolicyV4Options struct {
35
36
37
38
39 GoogleAccessID string
40
41
42
43
44
45
46
47
48
49
50
51
52
53 PrivateKey []byte
54
55
56
57
58
59
60
61
62
63
64
65
66 SignBytes func(hashBytes []byte) (signature []byte, err error)
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86 SignRawBytes func(bytes []byte) (signature []byte, err error)
87
88
89
90
91 Expires time.Time
92
93
94
95
96
97 Style URLStyle
98
99
100
101
102 Insecure bool
103
104
105
106
107
108 Fields *PolicyV4Fields
109
110
111
112
113
114 Conditions []PostPolicyV4Condition
115
116
117
118
119
120 Hostname string
121
122 shouldHashSignBytes bool
123 }
124
125 func (opts *PostPolicyV4Options) clone() *PostPolicyV4Options {
126 return &PostPolicyV4Options{
127 GoogleAccessID: opts.GoogleAccessID,
128 PrivateKey: opts.PrivateKey,
129 SignBytes: opts.SignBytes,
130 SignRawBytes: opts.SignRawBytes,
131 Expires: opts.Expires,
132 Style: opts.Style,
133 Insecure: opts.Insecure,
134 Fields: opts.Fields,
135 Conditions: opts.Conditions,
136 shouldHashSignBytes: opts.shouldHashSignBytes,
137 Hostname: opts.Hostname,
138 }
139 }
140
141
142 type PolicyV4Fields struct {
143
144
145 ACL string
146
147
148 CacheControl string
149
150
151 ContentType string
152
153
154 ContentDisposition string
155
156
157
158
159 ContentEncoding string
160
161
162
163 Metadata map[string]string
164
165
166
167 StatusCodeOnSuccess int
168
169
170
171 RedirectToURLOnSuccess string
172 }
173
174
175 type PostPolicyV4 struct {
176
177 URL string
178
179
180 Fields map[string]string
181 }
182
183
184
185 type PostPolicyV4Condition interface {
186 isEmpty() bool
187 json.Marshaler
188 }
189
190 type startsWith struct {
191 key, value string
192 }
193
194 func (sw *startsWith) MarshalJSON() ([]byte, error) {
195 return json.Marshal([]string{"starts-with", sw.key, sw.value})
196 }
197 func (sw *startsWith) isEmpty() bool {
198 return sw.value == ""
199 }
200
201
202
203 func ConditionStartsWith(key, value string) PostPolicyV4Condition {
204 return &startsWith{key, value}
205 }
206
207 type contentLengthRangeCondition struct {
208 start, end uint64
209 }
210
211 func (clr *contentLengthRangeCondition) MarshalJSON() ([]byte, error) {
212 return json.Marshal([]interface{}{"content-length-range", clr.start, clr.end})
213 }
214 func (clr *contentLengthRangeCondition) isEmpty() bool {
215 return clr.start == 0 && clr.end == 0
216 }
217
218 type singleValueCondition struct {
219 name, value string
220 }
221
222 func (svc *singleValueCondition) MarshalJSON() ([]byte, error) {
223 return json.Marshal(map[string]string{svc.name: svc.value})
224 }
225 func (svc *singleValueCondition) isEmpty() bool {
226 return svc.value == ""
227 }
228
229
230
231 func ConditionContentLengthRange(start, end uint64) PostPolicyV4Condition {
232 return &contentLengthRangeCondition{start, end}
233 }
234
235 func conditionRedirectToURLOnSuccess(redirectURL string) PostPolicyV4Condition {
236 return &singleValueCondition{"success_action_redirect", redirectURL}
237 }
238
239 func conditionStatusCodeOnSuccess(statusCode int) PostPolicyV4Condition {
240 svc := &singleValueCondition{name: "success_action_status"}
241 if statusCode > 0 {
242 svc.value = fmt.Sprintf("%d", statusCode)
243 }
244 return svc
245 }
246
247
248
249
250
251 func GenerateSignedPostPolicyV4(bucket, object string, opts *PostPolicyV4Options) (*PostPolicyV4, error) {
252 if bucket == "" {
253 return nil, errors.New("storage: bucket must be non-empty")
254 }
255 if object == "" {
256 return nil, errors.New("storage: object must be non-empty")
257 }
258 now := utcNow()
259 if err := validatePostPolicyV4Options(opts, now); err != nil {
260 return nil, err
261 }
262
263 var signingFn func(hashedBytes []byte) ([]byte, error)
264 switch {
265 case opts.SignRawBytes != nil:
266 signingFn = opts.SignRawBytes
267 case opts.shouldHashSignBytes:
268 signingFn = opts.SignBytes
269 case len(opts.PrivateKey) != 0:
270 parsedRSAPrivKey, err := parseKey(opts.PrivateKey)
271 if err != nil {
272 return nil, err
273 }
274 signingFn = func(b []byte) ([]byte, error) {
275 sum := sha256.Sum256(b)
276 return rsa.SignPKCS1v15(rand.Reader, parsedRSAPrivKey, crypto.SHA256, sum[:])
277 }
278
279 default:
280 return nil, errors.New("storage: exactly one of PrivateKey or SignRawBytes must be set")
281 }
282
283 var descFields PolicyV4Fields
284 if opts.Fields != nil {
285 descFields = *opts.Fields
286 }
287
288 if err := validateMetadata(descFields.Metadata); err != nil {
289 return nil, err
290 }
291
292
293 conds := make([]PostPolicyV4Condition, len(opts.Conditions))
294 copy(conds, opts.Conditions)
295 conds = append(conds,
296
297
298
299 &singleValueCondition{"acl", descFields.ACL},
300 &singleValueCondition{"cache-control", descFields.CacheControl},
301 &singleValueCondition{"content-disposition", descFields.ContentDisposition},
302 &singleValueCondition{"content-encoding", descFields.ContentEncoding},
303 &singleValueCondition{"content-type", descFields.ContentType},
304 conditionRedirectToURLOnSuccess(descFields.RedirectToURLOnSuccess),
305 conditionStatusCodeOnSuccess(descFields.StatusCodeOnSuccess),
306 )
307
308 YYYYMMDD := now.Format(yearMonthDay)
309 policyFields := map[string]string{
310 "key": object,
311 "x-goog-date": now.Format(iso8601),
312 "x-goog-credential": opts.GoogleAccessID + "/" + YYYYMMDD + "/auto/storage/goog4_request",
313 "x-goog-algorithm": "GOOG4-RSA-SHA256",
314 "acl": descFields.ACL,
315 "cache-control": descFields.CacheControl,
316 "content-disposition": descFields.ContentDisposition,
317 "content-encoding": descFields.ContentEncoding,
318 "content-type": descFields.ContentType,
319 "success_action_redirect": descFields.RedirectToURLOnSuccess,
320 }
321 for key, value := range descFields.Metadata {
322 conds = append(conds, &singleValueCondition{key, value})
323 policyFields[key] = value
324 }
325
326
327
328 conds = append(conds,
329 &singleValueCondition{"bucket", bucket},
330 &singleValueCondition{"key", object},
331 &singleValueCondition{"x-goog-date", now.Format(iso8601)},
332 &singleValueCondition{
333 name: "x-goog-credential",
334 value: opts.GoogleAccessID + "/" + YYYYMMDD + "/auto/storage/goog4_request",
335 },
336 &singleValueCondition{"x-goog-algorithm", "GOOG4-RSA-SHA256"},
337 )
338
339 nonEmptyConds := make([]PostPolicyV4Condition, 0, len(opts.Conditions))
340 for _, cond := range conds {
341 if cond == nil || !cond.isEmpty() {
342 nonEmptyConds = append(nonEmptyConds, cond)
343 }
344 }
345 condsAsJSON, err := json.Marshal(map[string]interface{}{
346 "conditions": nonEmptyConds,
347 "expiration": opts.Expires.Format(time.RFC3339),
348 })
349 if err != nil {
350 return nil, fmt.Errorf("storage: PostPolicyV4 JSON serialization failed: %w", err)
351 }
352
353 b64Policy := base64.StdEncoding.EncodeToString(condsAsJSON)
354 var signature []byte
355 var signErr error
356
357 if opts.shouldHashSignBytes {
358
359 shaSum := sha256.Sum256([]byte(b64Policy))
360 signature, signErr = signingFn(shaSum[:])
361 } else {
362 signature, signErr = signingFn([]byte(b64Policy))
363 }
364 if signErr != nil {
365 return nil, signErr
366 }
367
368 policyFields["policy"] = b64Policy
369 policyFields["x-goog-signature"] = fmt.Sprintf("%x", signature)
370
371
372 scheme := "https"
373 if opts.Insecure {
374 scheme = "http"
375 }
376 path := opts.Style.path(bucket, "") + "/"
377 u := &url.URL{
378 Path: path,
379 RawPath: pathEncodeV4(path),
380 Host: opts.Style.host(opts.Hostname, bucket),
381 Scheme: scheme,
382 }
383
384 if descFields.StatusCodeOnSuccess > 0 {
385 policyFields["success_action_status"] = fmt.Sprintf("%d", descFields.StatusCodeOnSuccess)
386 }
387
388
389 for key, value := range policyFields {
390 if value == "" {
391 delete(policyFields, key)
392 }
393 }
394 pp4 := &PostPolicyV4{
395 Fields: policyFields,
396 URL: u.String(),
397 }
398 return pp4, nil
399 }
400
401
402
403
404
405
406
407 func validatePostPolicyV4Options(opts *PostPolicyV4Options, now time.Time) error {
408 if opts == nil || opts.GoogleAccessID == "" {
409 return errors.New("storage: missing required GoogleAccessID")
410 }
411 if privBlank, signBlank := len(opts.PrivateKey) == 0, opts.SignBytes == nil && opts.SignRawBytes == nil; privBlank == signBlank {
412 return errors.New("storage: exactly one of PrivateKey or SignRawBytes must be set")
413 }
414 if opts.Expires.Before(now) {
415 return errors.New("storage: expecting Expires to be in the future")
416 }
417 if opts.Style == nil {
418 opts.Style = PathStyle()
419 }
420 if opts.SignRawBytes == nil && opts.SignBytes != nil {
421 opts.shouldHashSignBytes = true
422 }
423 return nil
424 }
425
426
427
428 func validateMetadata(hdrs map[string]string) (err error) {
429 if len(hdrs) == 0 {
430 return nil
431 }
432
433 badKeys := make([]string, 0, len(hdrs))
434 for key := range hdrs {
435 if !strings.HasPrefix(key, "x-goog-meta-") {
436 badKeys = append(badKeys, key)
437 }
438 }
439 if len(badKeys) != 0 {
440 err = errors.New("storage: expected metadata to begin with x-goog-meta-, got " + strings.Join(badKeys, ", "))
441 }
442 return
443 }
444
View as plain text