1/* 2 * MinIO Go Library for Amazon S3 Compatible Cloud Storage 3 * Copyright 2015-2017 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 18package minio 19 20import ( 21 "encoding/base64" 22 "fmt" 23 "strings" 24 "time" 25) 26 27// expirationDateFormat date format for expiration key in json policy. 28const expirationDateFormat = "2006-01-02T15:04:05.999Z" 29 30// policyCondition explanation: 31// http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html 32// 33// Example: 34// 35// policyCondition { 36// matchType: "$eq", 37// key: "$Content-Type", 38// value: "image/png", 39// } 40// 41type policyCondition struct { 42 matchType string 43 condition string 44 value string 45} 46 47// PostPolicy - Provides strict static type conversion and validation 48// for Amazon S3's POST policy JSON string. 49type PostPolicy struct { 50 // Expiration date and time of the POST policy. 51 expiration time.Time 52 // Collection of different policy conditions. 53 conditions []policyCondition 54 // ContentLengthRange minimum and maximum allowable size for the 55 // uploaded content. 56 contentLengthRange struct { 57 min int64 58 max int64 59 } 60 61 // Post form data. 62 formData map[string]string 63} 64 65// NewPostPolicy - Instantiate new post policy. 66func NewPostPolicy() *PostPolicy { 67 p := &PostPolicy{} 68 p.conditions = make([]policyCondition, 0) 69 p.formData = make(map[string]string) 70 return p 71} 72 73// SetExpires - Sets expiration time for the new policy. 74func (p *PostPolicy) SetExpires(t time.Time) error { 75 if t.IsZero() { 76 return errInvalidArgument("No expiry time set.") 77 } 78 p.expiration = t 79 return nil 80} 81 82// SetKey - Sets an object name for the policy based upload. 83func (p *PostPolicy) SetKey(key string) error { 84 if strings.TrimSpace(key) == "" || key == "" { 85 return errInvalidArgument("Object name is empty.") 86 } 87 policyCond := policyCondition{ 88 matchType: "eq", 89 condition: "$key", 90 value: key, 91 } 92 if err := p.addNewPolicy(policyCond); err != nil { 93 return err 94 } 95 p.formData["key"] = key 96 return nil 97} 98 99// SetKeyStartsWith - Sets an object name that an policy based upload 100// can start with. 101func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error { 102 if strings.TrimSpace(keyStartsWith) == "" || keyStartsWith == "" { 103 return errInvalidArgument("Object prefix is empty.") 104 } 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. 118func (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 135func (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. 157func (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. 175func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) error { 176 if strings.TrimSpace(contentTypeStartsWith) == "" || contentTypeStartsWith == "" { 177 return errInvalidArgument("No content type specified.") 178 } 179 policyCond := policyCondition{ 180 matchType: "starts-with", 181 condition: "$Content-Type", 182 value: contentTypeStartsWith, 183 } 184 if err := p.addNewPolicy(policyCond); err != nil { 185 return err 186 } 187 p.formData["Content-Type"] = contentTypeStartsWith 188 return nil 189} 190 191// SetContentLengthRange - Set new min and max content length 192// condition for all incoming uploads. 193func (p *PostPolicy) SetContentLengthRange(min, max int64) error { 194 if min > max { 195 return errInvalidArgument("Minimum limit is larger than maximum limit.") 196 } 197 if min < 0 { 198 return errInvalidArgument("Minimum limit cannot be negative.") 199 } 200 if max < 0 { 201 return errInvalidArgument("Maximum limit cannot be negative.") 202 } 203 p.contentLengthRange.min = min 204 p.contentLengthRange.max = max 205 return nil 206} 207 208// SetSuccessActionRedirect - Sets the redirect success url of the object for this policy 209// based upload. 210func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { 211 if strings.TrimSpace(redirect) == "" || redirect == "" { 212 return errInvalidArgument("Redirect is empty") 213 } 214 policyCond := policyCondition{ 215 matchType: "eq", 216 condition: "$success_action_redirect", 217 value: redirect, 218 } 219 if err := p.addNewPolicy(policyCond); err != nil { 220 return err 221 } 222 p.formData["success_action_redirect"] = redirect 223 return nil 224} 225 226// SetSuccessStatusAction - Sets the status success code of the object for this policy 227// based upload. 228func (p *PostPolicy) SetSuccessStatusAction(status string) error { 229 if strings.TrimSpace(status) == "" || status == "" { 230 return errInvalidArgument("Status is empty") 231 } 232 policyCond := policyCondition{ 233 matchType: "eq", 234 condition: "$success_action_status", 235 value: status, 236 } 237 if err := p.addNewPolicy(policyCond); err != nil { 238 return err 239 } 240 p.formData["success_action_status"] = status 241 return nil 242} 243 244// SetUserMetadata - Set user metadata as a key/value couple. 245// Can be retrieved through a HEAD request or an event. 246func (p *PostPolicy) SetUserMetadata(key string, value string) error { 247 if strings.TrimSpace(key) == "" || key == "" { 248 return errInvalidArgument("Key is empty") 249 } 250 if strings.TrimSpace(value) == "" || value == "" { 251 return errInvalidArgument("Value is empty") 252 } 253 headerName := fmt.Sprintf("x-amz-meta-%s", key) 254 policyCond := policyCondition{ 255 matchType: "eq", 256 condition: fmt.Sprintf("$%s", headerName), 257 value: value, 258 } 259 if err := p.addNewPolicy(policyCond); err != nil { 260 return err 261 } 262 p.formData[headerName] = value 263 return nil 264} 265 266// SetUserData - Set user data as a key/value couple. 267// Can be retrieved through a HEAD request or an event. 268func (p *PostPolicy) SetUserData(key string, value string) error { 269 if key == "" { 270 return errInvalidArgument("Key is empty") 271 } 272 if value == "" { 273 return errInvalidArgument("Value is empty") 274 } 275 headerName := fmt.Sprintf("x-amz-%s", key) 276 policyCond := policyCondition{ 277 matchType: "eq", 278 condition: fmt.Sprintf("$%s", headerName), 279 value: value, 280 } 281 if err := p.addNewPolicy(policyCond); err != nil { 282 return err 283 } 284 p.formData[headerName] = value 285 return nil 286} 287 288// addNewPolicy - internal helper to validate adding new policies. 289func (p *PostPolicy) addNewPolicy(policyCond policyCondition) error { 290 if policyCond.matchType == "" || policyCond.condition == "" || policyCond.value == "" { 291 return errInvalidArgument("Policy fields are empty.") 292 } 293 p.conditions = append(p.conditions, policyCond) 294 return nil 295} 296 297// String function for printing policy in json formatted string. 298func (p PostPolicy) String() string { 299 return string(p.marshalJSON()) 300} 301 302// marshalJSON - Provides Marshaled JSON in bytes. 303func (p PostPolicy) marshalJSON() []byte { 304 expirationStr := `"expiration":"` + p.expiration.Format(expirationDateFormat) + `"` 305 var conditionsStr string 306 conditions := []string{} 307 for _, po := range p.conditions { 308 conditions = append(conditions, fmt.Sprintf("[\"%s\",\"%s\",\"%s\"]", po.matchType, po.condition, po.value)) 309 } 310 if p.contentLengthRange.min != 0 || p.contentLengthRange.max != 0 { 311 conditions = append(conditions, fmt.Sprintf("[\"content-length-range\", %d, %d]", 312 p.contentLengthRange.min, p.contentLengthRange.max)) 313 } 314 if len(conditions) > 0 { 315 conditionsStr = `"conditions":[` + strings.Join(conditions, ",") + "]" 316 } 317 retStr := "{" 318 retStr = retStr + expirationStr + "," 319 retStr = retStr + conditionsStr 320 retStr = retStr + "}" 321 return []byte(retStr) 322} 323 324// base64 - Produces base64 of PostPolicy's Marshaled json. 325func (p PostPolicy) base64() string { 326 return base64.StdEncoding.EncodeToString(p.marshalJSON()) 327} 328