post-policy.go (10051B)
1 /* 2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage 3 * Copyright 2015-2023 MinIO, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package minio 19 20 import ( 21 "encoding/base64" 22 "fmt" 23 "net/http" 24 "strings" 25 "time" 26 27 "github.com/minio/minio-go/v7/pkg/encrypt" 28 ) 29 30 // expirationDateFormat date format for expiration key in json policy. 31 const expirationDateFormat = "2006-01-02T15:04:05.000Z" 32 33 // policyCondition explanation: 34 // http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html 35 // 36 // Example: 37 // 38 // policyCondition { 39 // matchType: "$eq", 40 // key: "$Content-Type", 41 // value: "image/png", 42 // } 43 type policyCondition struct { 44 matchType string 45 condition string 46 value string 47 } 48 49 // PostPolicy - Provides strict static type conversion and validation 50 // for Amazon S3's POST policy JSON string. 51 type PostPolicy struct { 52 // Expiration date and time of the POST policy. 53 expiration time.Time 54 // Collection of different policy conditions. 55 conditions []policyCondition 56 // ContentLengthRange minimum and maximum allowable size for the 57 // uploaded content. 58 contentLengthRange struct { 59 min int64 60 max int64 61 } 62 63 // Post form data. 64 formData map[string]string 65 } 66 67 // NewPostPolicy - Instantiate new post policy. 68 func NewPostPolicy() *PostPolicy { 69 p := &PostPolicy{} 70 p.conditions = make([]policyCondition, 0) 71 p.formData = make(map[string]string) 72 return p 73 } 74 75 // SetExpires - Sets expiration time for the new policy. 76 func (p *PostPolicy) SetExpires(t time.Time) error { 77 if t.IsZero() { 78 return errInvalidArgument("No expiry time set.") 79 } 80 p.expiration = t 81 return nil 82 } 83 84 // SetKey - Sets an object name for the policy based upload. 85 func (p *PostPolicy) SetKey(key string) error { 86 if strings.TrimSpace(key) == "" || key == "" { 87 return errInvalidArgument("Object name is empty.") 88 } 89 policyCond := policyCondition{ 90 matchType: "eq", 91 condition: "$key", 92 value: key, 93 } 94 if err := p.addNewPolicy(policyCond); err != nil { 95 return err 96 } 97 p.formData["key"] = key 98 return nil 99 } 100 101 // SetKeyStartsWith - Sets an object name that an policy based upload 102 // can start with. 103 // Can use an empty value ("") to allow any key. 104 func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error { 105 policyCond := policyCondition{ 106 matchType: "starts-with", 107 condition: "$key", 108 value: keyStartsWith, 109 } 110 if err := p.addNewPolicy(policyCond); err != nil { 111 return err 112 } 113 p.formData["key"] = keyStartsWith 114 return nil 115 } 116 117 // SetBucket - Sets bucket at which objects will be uploaded to. 118 func (p *PostPolicy) SetBucket(bucketName string) error { 119 if strings.TrimSpace(bucketName) == "" || bucketName == "" { 120 return errInvalidArgument("Bucket name is empty.") 121 } 122 policyCond := policyCondition{ 123 matchType: "eq", 124 condition: "$bucket", 125 value: bucketName, 126 } 127 if err := p.addNewPolicy(policyCond); err != nil { 128 return err 129 } 130 p.formData["bucket"] = bucketName 131 return nil 132 } 133 134 // SetCondition - Sets condition for credentials, date and algorithm 135 func (p *PostPolicy) SetCondition(matchType, condition, value string) error { 136 if strings.TrimSpace(value) == "" || value == "" { 137 return errInvalidArgument("No value specified for condition") 138 } 139 140 policyCond := policyCondition{ 141 matchType: matchType, 142 condition: "$" + condition, 143 value: value, 144 } 145 if condition == "X-Amz-Credential" || condition == "X-Amz-Date" || condition == "X-Amz-Algorithm" { 146 if err := p.addNewPolicy(policyCond); err != nil { 147 return err 148 } 149 p.formData[condition] = value 150 return nil 151 } 152 return errInvalidArgument("Invalid condition in policy") 153 } 154 155 // SetContentType - Sets content-type of the object for this policy 156 // based upload. 157 func (p *PostPolicy) SetContentType(contentType string) error { 158 if strings.TrimSpace(contentType) == "" || contentType == "" { 159 return errInvalidArgument("No content type specified.") 160 } 161 policyCond := policyCondition{ 162 matchType: "eq", 163 condition: "$Content-Type", 164 value: contentType, 165 } 166 if err := p.addNewPolicy(policyCond); err != nil { 167 return err 168 } 169 p.formData["Content-Type"] = contentType 170 return nil 171 } 172 173 // SetContentTypeStartsWith - Sets what content-type of the object for this policy 174 // based upload can start with. 175 // Can use an empty value ("") to allow any content-type. 176 func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) error { 177 policyCond := policyCondition{ 178 matchType: "starts-with", 179 condition: "$Content-Type", 180 value: contentTypeStartsWith, 181 } 182 if err := p.addNewPolicy(policyCond); err != nil { 183 return err 184 } 185 p.formData["Content-Type"] = contentTypeStartsWith 186 return nil 187 } 188 189 // SetContentLengthRange - Set new min and max content length 190 // condition for all incoming uploads. 191 func (p *PostPolicy) SetContentLengthRange(min, max int64) error { 192 if min > max { 193 return errInvalidArgument("Minimum limit is larger than maximum limit.") 194 } 195 if min < 0 { 196 return errInvalidArgument("Minimum limit cannot be negative.") 197 } 198 if max <= 0 { 199 return errInvalidArgument("Maximum limit cannot be non-positive.") 200 } 201 p.contentLengthRange.min = min 202 p.contentLengthRange.max = max 203 return nil 204 } 205 206 // SetSuccessActionRedirect - Sets the redirect success url of the object for this policy 207 // based upload. 208 func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { 209 if strings.TrimSpace(redirect) == "" || redirect == "" { 210 return errInvalidArgument("Redirect is empty") 211 } 212 policyCond := policyCondition{ 213 matchType: "eq", 214 condition: "$success_action_redirect", 215 value: redirect, 216 } 217 if err := p.addNewPolicy(policyCond); err != nil { 218 return err 219 } 220 p.formData["success_action_redirect"] = redirect 221 return nil 222 } 223 224 // SetSuccessStatusAction - Sets the status success code of the object for this policy 225 // based upload. 226 func (p *PostPolicy) SetSuccessStatusAction(status string) error { 227 if strings.TrimSpace(status) == "" || status == "" { 228 return errInvalidArgument("Status is empty") 229 } 230 policyCond := policyCondition{ 231 matchType: "eq", 232 condition: "$success_action_status", 233 value: status, 234 } 235 if err := p.addNewPolicy(policyCond); err != nil { 236 return err 237 } 238 p.formData["success_action_status"] = status 239 return nil 240 } 241 242 // SetUserMetadata - Set user metadata as a key/value couple. 243 // Can be retrieved through a HEAD request or an event. 244 func (p *PostPolicy) SetUserMetadata(key string, value string) error { 245 if strings.TrimSpace(key) == "" || key == "" { 246 return errInvalidArgument("Key is empty") 247 } 248 if strings.TrimSpace(value) == "" || value == "" { 249 return errInvalidArgument("Value is empty") 250 } 251 headerName := fmt.Sprintf("x-amz-meta-%s", key) 252 policyCond := policyCondition{ 253 matchType: "eq", 254 condition: fmt.Sprintf("$%s", headerName), 255 value: value, 256 } 257 if err := p.addNewPolicy(policyCond); err != nil { 258 return err 259 } 260 p.formData[headerName] = value 261 return nil 262 } 263 264 // SetChecksum sets the checksum of the request. 265 func (p *PostPolicy) SetChecksum(c Checksum) { 266 if c.IsSet() { 267 p.formData[amzChecksumAlgo] = c.Type.String() 268 p.formData[c.Type.Key()] = c.Encoded() 269 } 270 } 271 272 // SetEncryption - sets encryption headers for POST API 273 func (p *PostPolicy) SetEncryption(sse encrypt.ServerSide) { 274 if sse == nil { 275 return 276 } 277 h := http.Header{} 278 sse.Marshal(h) 279 for k, v := range h { 280 p.formData[k] = v[0] 281 } 282 } 283 284 // SetUserData - Set user data as a key/value couple. 285 // Can be retrieved through a HEAD request or an event. 286 func (p *PostPolicy) SetUserData(key string, value string) error { 287 if key == "" { 288 return errInvalidArgument("Key is empty") 289 } 290 if value == "" { 291 return errInvalidArgument("Value is empty") 292 } 293 headerName := fmt.Sprintf("x-amz-%s", key) 294 policyCond := policyCondition{ 295 matchType: "eq", 296 condition: fmt.Sprintf("$%s", headerName), 297 value: value, 298 } 299 if err := p.addNewPolicy(policyCond); err != nil { 300 return err 301 } 302 p.formData[headerName] = value 303 return nil 304 } 305 306 // addNewPolicy - internal helper to validate adding new policies. 307 // Can use starts-with with an empty value ("") to allow any content within a form field. 308 func (p *PostPolicy) addNewPolicy(policyCond policyCondition) error { 309 if policyCond.matchType == "" || policyCond.condition == "" { 310 return errInvalidArgument("Policy fields are empty.") 311 } 312 if policyCond.matchType != "starts-with" && policyCond.value == "" { 313 return errInvalidArgument("Policy value is empty.") 314 } 315 p.conditions = append(p.conditions, policyCond) 316 return nil 317 } 318 319 // String function for printing policy in json formatted string. 320 func (p PostPolicy) String() string { 321 return string(p.marshalJSON()) 322 } 323 324 // marshalJSON - Provides Marshaled JSON in bytes. 325 func (p PostPolicy) marshalJSON() []byte { 326 expirationStr := `"expiration":"` + p.expiration.Format(expirationDateFormat) + `"` 327 var conditionsStr string 328 conditions := []string{} 329 for _, po := range p.conditions { 330 conditions = append(conditions, fmt.Sprintf("[\"%s\",\"%s\",\"%s\"]", po.matchType, po.condition, po.value)) 331 } 332 if p.contentLengthRange.min != 0 || p.contentLengthRange.max != 0 { 333 conditions = append(conditions, fmt.Sprintf("[\"content-length-range\", %d, %d]", 334 p.contentLengthRange.min, p.contentLengthRange.max)) 335 } 336 if len(conditions) > 0 { 337 conditionsStr = `"conditions":[` + strings.Join(conditions, ",") + "]" 338 } 339 retStr := "{" 340 retStr = retStr + expirationStr + "," 341 retStr += conditionsStr 342 retStr += "}" 343 return []byte(retStr) 344 } 345 346 // base64 - Produces base64 of PostPolicy's Marshaled json. 347 func (p PostPolicy) base64() string { 348 return base64.StdEncoding.EncodeToString(p.marshalJSON()) 349 }