Copyright 2020 Google LLC 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 storage

import (
	
	
	
	
	
	
	
	
	
	
	
)
PostPolicyV4Options are used to construct a signed post policy. Please see https://cloud.google.com/storage/docs/xml-api/post-object for reference about the fields.
GoogleAccessID represents the authorizer of the signed URL generation. It is typically the Google service account client email address from the Google Developers Console in the form of "xxx@developer.gserviceaccount.com". Required.
PrivateKey is the Google service account private key. It is obtainable from the Google Developers Console. At https://console.developers.google.com/project/<your-project-id>/apiui/credential, create a service account client ID or reuse one of your existing service account credentials. Click on the "Generate new P12 key" to generate and download a new private key. Once you download the P12 file, use the following command to convert it into a PEM file. $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes Provide the contents of the PEM file as a byte slice. Exactly one of PrivateKey or SignBytes must be non-nil.
SignBytes is a function for implementing custom signing. For example, if your application is running on Google App Engine, you can use appengine's internal signing function: ctx := appengine.NewContext(request) acc, _ := appengine.ServiceAccount(ctx) url, err := SignedURL("bucket", "object", &SignedURLOptions{ GoogleAccessID: acc, SignBytes: func(b []byte) ([]byte, error) { _, signedBytes, err := appengine.SignBytes(ctx, b) return signedBytes, err }, // etc. }) Exactly one of PrivateKey or SignBytes must be non-nil.
	SignBytes func(hashBytes []byte) (signature []byte, err error)
Expires is the expiration time on the signed URL. It must be a time in the future. Required.
Style provides options for the type of URL to use. Options are PathStyle (default), BucketBoundHostname, and VirtualHostedStyle. See https://cloud.google.com/storage/docs/request-endpoints for details. Optional.
Insecure when set indicates that the generated URL's scheme will use "http" instead of "https" (default). Optional.
Fields specifies the attributes of a PostPolicyV4 request. When Fields is non-nil, its attributes must match those that will passed into field Conditions. Optional.
The conditions that the uploaded file will be expected to conform to. When used, the failure of an upload to satisfy a condition will result in a 4XX status code, back with the message describing the problem. Optional.
PolicyV4Fields describes the attributes for a PostPolicyV4 request.
ACL specifies the access control permissions for the object. Optional.
CacheControl specifies the caching directives for the object. Optional.
ContentType specifies the media type of the object. Optional.
ContentDisposition specifies how the file will be served back to requesters. Optional.
ContentEncoding specifies the decompressive transcoding that the object. This field is complementary to ContentType in that the file could be compressed but ContentType specifies the file's original media type. Optional.
Metadata specifies custom metadata for the object. If any key doesn't begin with "x-goog-meta-", an error will be returned. Optional.
StatusCodeOnSuccess when set, specifies the status code that Cloud Storage will serve back on successful upload of the object. Optional.
RedirectToURLOnSuccess when set, specifies the URL that Cloud Storage will serve back on successful upload of the object. Optional.
PostPolicyV4 describes the URL and respective form fields for a generated PostPolicyV4 request.
URL is the generated URL that the file upload will be made to.
Fields specifies the generated key-values that the file uploader must include in their multipart upload form.
PostPolicyV4Condition describes the constraints that the subsequent object upload's multipart form fields will be expected to conform to.
type PostPolicyV4Condition interface {
	isEmpty() bool
	json.Marshaler
}

type startsWith struct {
	key, value string
}

func ( *startsWith) () ([]byte, error) {
	return json.Marshal([]string{"starts-with", .key, .value})
}
func ( *startsWith) () bool {
	return .value == ""
}
ConditionStartsWith checks that an attributes starts with value. An empty value will cause this condition to be ignored.
func (,  string) PostPolicyV4Condition {
	return &startsWith{, }
}

type contentLengthRangeCondition struct {
	start, end uint64
}

func ( *contentLengthRangeCondition) () ([]byte, error) {
	return json.Marshal([]interface{}{"content-length-range", .start, .end})
}
func ( *contentLengthRangeCondition) () bool {
	return .start == 0 && .end == 0
}

type singleValueCondition struct {
	name, value string
}

func ( *singleValueCondition) () ([]byte, error) {
	return json.Marshal(map[string]string{.name: .value})
}
func ( *singleValueCondition) () bool {
	return .value == ""
}
ConditionContentLengthRange constraints the limits that the multipart upload's range header will be expected to be within.
func (,  uint64) PostPolicyV4Condition {
	return &contentLengthRangeCondition{, }
}

func ( string) PostPolicyV4Condition {
	return &singleValueCondition{"success_action_redirect", }
}

func ( int) PostPolicyV4Condition {
	 := &singleValueCondition{name: "success_action_status"}
	if  > 0 {
		.value = fmt.Sprintf("%d", )
	}
	return 
}
GenerateSignedPostPolicyV4 generates a PostPolicyV4 value from bucket, object and opts. The generated URL and fields will then allow an unauthenticated client to perform multipart uploads.
func (,  string,  *PostPolicyV4Options) (*PostPolicyV4, error) {
	if  == "" {
		return nil, errors.New("storage: bucket must be non-empty")
	}
	if  == "" {
		return nil, errors.New("storage: object must be non-empty")
	}
	 := utcNow()
	if  := validatePostPolicyV4Options(, );  != nil {
		return nil, 
	}

	var  func( []byte) ([]byte, error)
	switch {
	case .SignBytes != nil:
		 = .SignBytes

	case len(.PrivateKey) != 0:
		,  := parseKey(.PrivateKey)
		if  != nil {
			return nil, 
		}
		 = func( []byte) ([]byte, error) {
			return rsa.SignPKCS1v15(rand.Reader, , crypto.SHA256, )
		}

	default:
		return nil, errors.New("storage: exactly one of PrivateKey or SignedBytes must be set")
	}

	var  PolicyV4Fields
	if .Fields != nil {
		 = *.Fields
	}

	if  := validateMetadata(.Metadata);  != nil {
		return nil, 
	}
Build the policy.
	 := make([]PostPolicyV4Condition, len(.Conditions))
	copy(, .Conditions)
	 = append(,
		conditionRedirectToURLOnSuccess(.RedirectToURLOnSuccess),
		conditionStatusCodeOnSuccess(.StatusCodeOnSuccess),
		&singleValueCondition{"acl", .ACL},
		&singleValueCondition{"cache-control", .CacheControl},
	)

	 := .Format(yearMonthDay)
	 := map[string]string{
		"key":                     ,
		"x-goog-date":             .Format(iso8601),
		"x-goog-credential":       .GoogleAccessID + "/" +  + "/auto/storage/goog4_request",
		"x-goog-algorithm":        "GOOG4-RSA-SHA256",
		"success_action_redirect": .RedirectToURLOnSuccess,
		"acl":                     .ACL,
	}
	for ,  := range .Metadata {
		 = append(, &singleValueCondition{, })
		[] = 
	}
Following from the order expected by the conformance test cases, hence manually inserting these fields in a specific order.
	 = append(,
		&singleValueCondition{"bucket", },
		&singleValueCondition{"key", },
		&singleValueCondition{"x-goog-date", .Format(iso8601)},
		&singleValueCondition{
			name:  "x-goog-credential",
			value: .GoogleAccessID + "/" +  + "/auto/storage/goog4_request",
		},
		&singleValueCondition{"x-goog-algorithm", "GOOG4-RSA-SHA256"},
	)

	 := make([]PostPolicyV4Condition, 0, len(.Conditions))
	for ,  := range  {
		if  == nil || !.isEmpty() {
			 = append(, )
		}
	}
	,  := json.Marshal(map[string]interface{}{
		"conditions": ,
		"expiration": .Expires.Format(time.RFC3339),
	})
	if  != nil {
		return nil, fmt.Errorf("storage: PostPolicyV4 JSON serialization failed: %v", )
	}

	 := base64.StdEncoding.EncodeToString()
	 := sha256.Sum256([]byte())
	,  := ([:])
	if  != nil {
		return nil, 
	}

	["policy"] = 
	["x-goog-signature"] = fmt.Sprintf("%x", )
Construct the URL.
	 := "https"
	if .Insecure {
		 = "http"
	}
	 := .Style.path(, "") + "/"
	 := &url.URL{
		Path:    ,
		RawPath: pathEncodeV4(),
		Host:    .Style.host(),
		Scheme:  ,
	}

	if .StatusCodeOnSuccess > 0 {
		["success_action_status"] = fmt.Sprintf("%d", .StatusCodeOnSuccess)
	}
Clear out fields with blanks values.
	for ,  := range  {
		if  == "" {
			delete(, )
		}
	}
	 := &PostPolicyV4{
		Fields: ,
		URL:    .String(),
	}
	return , nil
}
validatePostPolicyV4Options checks that: * GoogleAccessID is set * either but not both PrivateKey and SignBytes are set or nil, but not both * Expires, the deadline is not in the past * if Style is not set, it'll use PathStyle
func ( *PostPolicyV4Options,  time.Time) error {
	if  == nil || .GoogleAccessID == "" {
		return errors.New("storage: missing required GoogleAccessID")
	}
	if ,  := len(.PrivateKey) == 0, .SignBytes == nil;  ==  {
		return errors.New("storage: exactly one of PrivateKey or SignedBytes must be set")
	}
	if .Expires.Before() {
		return errors.New("storage: expecting Expires to be in the future")
	}
	if .Style == nil {
		.Style = PathStyle()
	}
	return nil
}
validateMetadata ensures that all keys passed in have a prefix of "x-goog-meta-", otherwise it will return an error.
func ( map[string]string) ( error) {
	if len() == 0 {
		return nil
	}

	 := make([]string, 0, len())
	for  := range  {
		if !strings.HasPrefix(, "x-goog-meta-") {
			 = append(, )
		}
	}
	if len() != 0 {
		 = errors.New("storage: expected metadata to begin with x-goog-meta-, got " + strings.Join(, ", "))
	}
	return