/* * MinIO Go Library for Amazon S3 Compatible Cloud Storage * Copyright 2015-2023 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package minio import ( "encoding/base64" "errors" "fmt" "net/http" "strings" "time" "github.com/minio/minio-go/v7/pkg/encrypt" "github.com/minio/minio-go/v7/pkg/tags" ) // expirationDateFormat date format for expiration key in json policy. const expirationDateFormat = "2006-01-02T15:04:05.000Z" // policyCondition explanation: // http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html // // Example: // // policyCondition { // matchType: "$eq", // key: "$Content-Type", // value: "image/png", // } type policyCondition struct { matchType string condition string value string } // PostPolicy - Provides strict static type conversion and validation // for Amazon S3's POST policy JSON string. type PostPolicy struct { // Expiration date and time of the POST policy. expiration time.Time // Collection of different policy conditions. conditions []policyCondition // ContentLengthRange minimum and maximum allowable size for the // uploaded content. contentLengthRange struct { min int64 max int64 } // Post form data. formData map[string]string } // NewPostPolicy - Instantiate new post policy. func NewPostPolicy() *PostPolicy { p := &PostPolicy{} p.conditions = make([]policyCondition, 0) p.formData = make(map[string]string) return p } // SetExpires - Sets expiration time for the new policy. func (p *PostPolicy) SetExpires(t time.Time) error { if t.IsZero() { return errInvalidArgument("No expiry time set.") } p.expiration = t return nil } // SetKey - Sets an object name for the policy based upload. func (p *PostPolicy) SetKey(key string) error { if strings.TrimSpace(key) == "" { return errInvalidArgument("Object name is empty.") } policyCond := policyCondition{ matchType: "eq", condition: "$key", value: key, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["key"] = key return nil } // SetKeyStartsWith - Sets an object name that an policy based upload // can start with. // Can use an empty value ("") to allow any key. func (p *PostPolicy) SetKeyStartsWith(keyStartsWith string) error { policyCond := policyCondition{ matchType: "starts-with", condition: "$key", value: keyStartsWith, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["key"] = keyStartsWith return nil } // SetBucket - Sets bucket at which objects will be uploaded to. func (p *PostPolicy) SetBucket(bucketName string) error { if strings.TrimSpace(bucketName) == "" { return errInvalidArgument("Bucket name is empty.") } policyCond := policyCondition{ matchType: "eq", condition: "$bucket", value: bucketName, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["bucket"] = bucketName return nil } // SetCondition - Sets condition for credentials, date and algorithm func (p *PostPolicy) SetCondition(matchType, condition, value string) error { if strings.TrimSpace(value) == "" { return errInvalidArgument("No value specified for condition") } policyCond := policyCondition{ matchType: matchType, condition: "$" + condition, value: value, } if condition == "X-Amz-Credential" || condition == "X-Amz-Date" || condition == "X-Amz-Algorithm" { if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData[condition] = value return nil } return errInvalidArgument("Invalid condition in policy") } // SetTagging - Sets tagging for the object for this policy based upload. func (p *PostPolicy) SetTagging(tagging string) error { if strings.TrimSpace(tagging) == "" { return errInvalidArgument("No tagging specified.") } _, err := tags.ParseObjectXML(strings.NewReader(tagging)) if err != nil { return errors.New("The XML you provided was not well-formed or did not validate against our published schema.") //nolint } policyCond := policyCondition{ matchType: "eq", condition: "$tagging", value: tagging, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["tagging"] = tagging return nil } // SetContentType - Sets content-type of the object for this policy // based upload. func (p *PostPolicy) SetContentType(contentType string) error { if strings.TrimSpace(contentType) == "" { return errInvalidArgument("No content type specified.") } policyCond := policyCondition{ matchType: "eq", condition: "$Content-Type", value: contentType, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["Content-Type"] = contentType return nil } // SetContentTypeStartsWith - Sets what content-type of the object for this policy // based upload can start with. // Can use an empty value ("") to allow any content-type. func (p *PostPolicy) SetContentTypeStartsWith(contentTypeStartsWith string) error { policyCond := policyCondition{ matchType: "starts-with", condition: "$Content-Type", value: contentTypeStartsWith, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["Content-Type"] = contentTypeStartsWith return nil } // SetContentDisposition - Sets content-disposition of the object for this policy func (p *PostPolicy) SetContentDisposition(contentDisposition string) error { if strings.TrimSpace(contentDisposition) == "" { return errInvalidArgument("No content disposition specified.") } policyCond := policyCondition{ matchType: "eq", condition: "$Content-Disposition", value: contentDisposition, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["Content-Disposition"] = contentDisposition return nil } // SetContentEncoding - Sets content-encoding of the object for this policy func (p *PostPolicy) SetContentEncoding(contentEncoding string) error { if strings.TrimSpace(contentEncoding) == "" { return errInvalidArgument("No content encoding specified.") } policyCond := policyCondition{ matchType: "eq", condition: "$Content-Encoding", value: contentEncoding, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["Content-Encoding"] = contentEncoding return nil } // SetContentLengthRange - Set new min and max content length // condition for all incoming uploads. func (p *PostPolicy) SetContentLengthRange(minLen, maxLen int64) error { if minLen > maxLen { return errInvalidArgument("Minimum limit is larger than maximum limit.") } if minLen < 0 { return errInvalidArgument("Minimum limit cannot be negative.") } if maxLen <= 0 { return errInvalidArgument("Maximum limit cannot be non-positive.") } p.contentLengthRange.min = minLen p.contentLengthRange.max = maxLen return nil } // SetSuccessActionRedirect - Sets the redirect success url of the object for this policy // based upload. func (p *PostPolicy) SetSuccessActionRedirect(redirect string) error { if strings.TrimSpace(redirect) == "" { return errInvalidArgument("Redirect is empty") } policyCond := policyCondition{ matchType: "eq", condition: "$success_action_redirect", value: redirect, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["success_action_redirect"] = redirect return nil } // SetSuccessStatusAction - Sets the status success code of the object for this policy // based upload. func (p *PostPolicy) SetSuccessStatusAction(status string) error { if strings.TrimSpace(status) == "" { return errInvalidArgument("Status is empty") } policyCond := policyCondition{ matchType: "eq", condition: "$success_action_status", value: status, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData["success_action_status"] = status return nil } // SetUserMetadata - Set user metadata as a key/value couple. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadata(key, value string) error { if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } if strings.TrimSpace(value) == "" { return errInvalidArgument("Value is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) policyCond := policyCondition{ matchType: "eq", condition: fmt.Sprintf("$%s", headerName), value: value, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData[headerName] = value return nil } // SetUserMetadataStartsWith - Set how an user metadata should starts with. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserMetadataStartsWith(key, value string) error { if strings.TrimSpace(key) == "" { return errInvalidArgument("Key is empty") } headerName := fmt.Sprintf("x-amz-meta-%s", key) policyCond := policyCondition{ matchType: "starts-with", condition: fmt.Sprintf("$%s", headerName), value: value, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData[headerName] = value return nil } // SetChecksum sets the checksum of the request. func (p *PostPolicy) SetChecksum(c Checksum) error { if c.IsSet() { p.formData[amzChecksumAlgo] = c.Type.String() p.formData[c.Type.Key()] = c.Encoded() policyCond := policyCondition{ matchType: "eq", condition: fmt.Sprintf("$%s", amzChecksumAlgo), value: c.Type.String(), } if err := p.addNewPolicy(policyCond); err != nil { return err } policyCond = policyCondition{ matchType: "eq", condition: fmt.Sprintf("$%s", c.Type.Key()), value: c.Encoded(), } if err := p.addNewPolicy(policyCond); err != nil { return err } } return nil } // SetEncryption - sets encryption headers for POST API func (p *PostPolicy) SetEncryption(sse encrypt.ServerSide) { if sse == nil { return } h := http.Header{} sse.Marshal(h) for k, v := range h { p.formData[k] = v[0] } } // SetUserData - Set user data as a key/value couple. // Can be retrieved through a HEAD request or an event. func (p *PostPolicy) SetUserData(key, value string) error { if key == "" { return errInvalidArgument("Key is empty") } if value == "" { return errInvalidArgument("Value is empty") } headerName := fmt.Sprintf("x-amz-%s", key) policyCond := policyCondition{ matchType: "eq", condition: fmt.Sprintf("$%s", headerName), value: value, } if err := p.addNewPolicy(policyCond); err != nil { return err } p.formData[headerName] = value return nil } // addNewPolicy - internal helper to validate adding new policies. // Can use starts-with with an empty value ("") to allow any content within a form field. func (p *PostPolicy) addNewPolicy(policyCond policyCondition) error { if policyCond.matchType == "" || policyCond.condition == "" { return errInvalidArgument("Policy fields are empty.") } if policyCond.matchType != "starts-with" && policyCond.value == "" { return errInvalidArgument("Policy value is empty.") } p.conditions = append(p.conditions, policyCond) return nil } // String function for printing policy in json formatted string. func (p PostPolicy) String() string { return string(p.marshalJSON()) } // marshalJSON - Provides Marshaled JSON in bytes. func (p PostPolicy) marshalJSON() []byte { expirationStr := `"expiration":"` + p.expiration.Format(expirationDateFormat) + `"` var conditionsStr string conditions := []string{} for _, po := range p.conditions { conditions = append(conditions, fmt.Sprintf("[\"%s\",\"%s\",\"%s\"]", po.matchType, po.condition, po.value)) } if p.contentLengthRange.min != 0 || p.contentLengthRange.max != 0 { conditions = append(conditions, fmt.Sprintf("[\"content-length-range\", %d, %d]", p.contentLengthRange.min, p.contentLengthRange.max)) } if len(conditions) > 0 { conditionsStr = `"conditions":[` + strings.Join(conditions, ",") + "]" } retStr := "{" retStr = retStr + expirationStr + "," retStr += conditionsStr retStr += "}" return []byte(retStr) } // base64 - Produces base64 of PostPolicy's Marshaled json. func (p PostPolicy) base64() string { return base64.StdEncoding.EncodeToString(p.marshalJSON()) }