From 251627c92c0b6b56da2683badf4cbd9b328f9a5a Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Thu, 4 Feb 2021 12:17:52 -0800 Subject: [PATCH] Add support for extra signed headers in Presign() API Fixes #1415 --- api-presigned.go | 29 +++++++------- api.go | 19 +++++++--- functional_tests.go | 92 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 116 insertions(+), 24 deletions(-) diff --git a/api-presigned.go b/api-presigned.go index 80c363da5b..26297ab634 100644 --- a/api-presigned.go +++ b/api-presigned.go @@ -30,7 +30,7 @@ import ( // presignURL - Returns a presigned URL for an input 'method'. // Expires maximum is 7days - ie. 604800 and minimum is 1. -func (c Client) presignURL(ctx context.Context, method string, bucketName string, objectName string, expires time.Duration, reqParams url.Values) (u *url.URL, err error) { +func (c Client) presignURL(ctx context.Context, method string, bucketName string, objectName string, expires time.Duration, reqParams url.Values, extraHeaders http.Header) (u *url.URL, err error) { // Input validation. if method == "" { return nil, errInvalidArgument("method cannot be empty.") @@ -45,11 +45,12 @@ func (c Client) presignURL(ctx context.Context, method string, bucketName string // Convert expires into seconds. expireSeconds := int64(expires / time.Second) reqMetadata := requestMetadata{ - presignURL: true, - bucketName: bucketName, - objectName: objectName, - expires: expireSeconds, - queryValues: reqParams, + presignURL: true, + bucketName: bucketName, + objectName: objectName, + expires: expireSeconds, + queryValues: reqParams, + extraPresignHeader: extraHeaders, } // Instantiate a new request. @@ -69,7 +70,7 @@ func (c Client) PresignedGetObject(ctx context.Context, bucketName string, objec if err = s3utils.CheckValidObjectName(objectName); err != nil { return nil, err } - return c.presignURL(ctx, http.MethodGet, bucketName, objectName, expires, reqParams) + return c.presignURL(ctx, http.MethodGet, bucketName, objectName, expires, reqParams, nil) } // PresignedHeadObject - Returns a presigned URL to access @@ -80,7 +81,7 @@ func (c Client) PresignedHeadObject(ctx context.Context, bucketName string, obje if err = s3utils.CheckValidObjectName(objectName); err != nil { return nil, err } - return c.presignURL(ctx, http.MethodHead, bucketName, objectName, expires, reqParams) + return c.presignURL(ctx, http.MethodHead, bucketName, objectName, expires, reqParams, nil) } // PresignedPutObject - Returns a presigned URL to upload an object @@ -90,14 +91,14 @@ func (c Client) PresignedPutObject(ctx context.Context, bucketName string, objec if err = s3utils.CheckValidObjectName(objectName); err != nil { return nil, err } - return c.presignURL(ctx, http.MethodPut, bucketName, objectName, expires, nil) + return c.presignURL(ctx, http.MethodPut, bucketName, objectName, expires, nil, nil) } -// Presign - returns a presigned URL for any http method of your choice -// along with custom request params. URL can have a maximum expiry of -// upto 7days or a minimum of 1sec. -func (c Client) Presign(ctx context.Context, method string, bucketName string, objectName string, expires time.Duration, reqParams url.Values) (u *url.URL, err error) { - return c.presignURL(ctx, method, bucketName, objectName, expires, reqParams) +// Presign - returns a presigned URL for any http method of your choice along +// with custom request params and extra signed headers. URL can have a maximum +// expiry of upto 7days or a minimum of 1sec. +func (c Client) Presign(ctx context.Context, method string, bucketName string, objectName string, expires time.Duration, reqParams url.Values, extraHeaders http.Header) (u *url.URL, err error) { + return c.presignURL(ctx, method, bucketName, objectName, expires, reqParams, extraHeaders) } // PresignedPostPolicy - Returns POST urlString, form data to upload an object. diff --git a/api.go b/api.go index f14bb0c603..7730b68633 100644 --- a/api.go +++ b/api.go @@ -393,11 +393,12 @@ type requestMetadata struct { presignURL bool // User supplied. - bucketName string - objectName string - queryValues url.Values - customHeader http.Header - expires int64 + bucketName string + objectName string + queryValues url.Values + customHeader http.Header + extraPresignHeader http.Header + expires int64 // Generated by our internal code. bucketLocation string @@ -736,6 +737,14 @@ func (c Client) newRequest(ctx context.Context, method string, metadata requestM if signerType.IsAnonymous() { return nil, errInvalidArgument("Presigned URLs cannot be generated with anonymous credentials.") } + if metadata.extraPresignHeader != nil { + if signerType.IsV2() { + return nil, errInvalidArgument("Extra signed headers for Presign with Signature V2 is not supported.") + } + for k, v := range metadata.extraPresignHeader { + req.Header.Set(k, v[0]) + } + } if signerType.IsV2() { // Presign URL with signature v2. req = signer.PreSignV2(*req, accessKeyID, secretAccessKey, metadata.expires, isVirtualHost) diff --git a/functional_tests.go b/functional_tests.go index 8758c8edd6..76410addd9 100644 --- a/functional_tests.go +++ b/functional_tests.go @@ -5540,6 +5540,63 @@ func testFunctional() { return } + function = "Presign(method, bucketName, objectName, expires, reqParams, extraHeaders)" + functionAll += ", " + function + presignExtraHeaders := map[string][]string{ + "mysecret": {"abcxxx"}, + } + args = map[string]interface{}{ + "method": "PUT", + "bucketName": bucketName, + "objectName": objectName + "-presign-custom", + "expires": 3600 * time.Second, + "extraHeaders": presignExtraHeaders, + } + presignedURL, err := c.Presign(context.Background(), "PUT", bucketName, objectName+"-presign-custom", 3600*time.Second, nil, presignExtraHeaders) + if err != nil { + logError(testName, function, args, startTime, "", "Presigned failed", err) + return + } + + // Generate data more than 32K + buf = bytes.Repeat([]byte("1"), rand.Intn(1<<10)+32*1024) + + req, err = http.NewRequest(http.MethodPut, presignedURL.String(), bytes.NewReader(buf)) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request to Presigned URL failed", err) + return + } + + req.Header.Add("mysecret", "abcxxx") + resp, err = httpClient.Do(req) + if err != nil { + logError(testName, function, args, startTime, "", "HTTP request to Presigned URL failed", err) + return + } + + // Download the uploaded object to verify + args = map[string]interface{}{ + "bucketName": bucketName, + "objectName": objectName + "-presign-custom", + } + newReader, err = c.GetObject(context.Background(), bucketName, objectName+"-presign-custom", minio.GetObjectOptions{}) + if err != nil { + logError(testName, function, args, startTime, "", "GetObject of uploaded custom-presigned object failed", err) + return + } + + newReadBytes, err = ioutil.ReadAll(newReader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed during get on custom-presigned put object", err) + return + } + newReader.Close() + + if !bytes.Equal(newReadBytes, buf) { + logError(testName, function, args, startTime, "", "Bytes mismatch on custom-presigned object upload verification", err) + return + } + function = "RemoveObject(bucketName, objectName)" functionAll += ", " + function args = map[string]interface{}{ @@ -5576,6 +5633,14 @@ func testFunctional() { return } + args["objectName"] = objectName + "-presign-custom" + err = c.RemoveObject(context.Background(), bucketName, objectName+"-presign-custom", minio.RemoveObjectOptions{}) + + if err != nil { + logError(testName, function, args, startTime, "", "RemoveObject failed", err) + return + } + function = "RemoveBucket(bucketName)" functionAll += ", " + function args = map[string]interface{}{ @@ -10318,27 +10383,44 @@ func testFunctionalV2() { return } - function = "GetObject(bucketName, objectName)" - functionAll += ", " + function + // Download the uploaded object to verify args = map[string]interface{}{ "bucketName": bucketName, "objectName": objectName + "-presigned", } newReader, err = c.GetObject(context.Background(), bucketName, objectName+"-presigned", minio.GetObjectOptions{}) if err != nil { - logError(testName, function, args, startTime, "", "GetObject failed", err) + logError(testName, function, args, startTime, "", "GetObject of uploaded presigned object failed", err) return } newReadBytes, err = ioutil.ReadAll(newReader) if err != nil { - logError(testName, function, args, startTime, "", "ReadAll failed", err) + logError(testName, function, args, startTime, "", "ReadAll failed during get on presigned put object", err) return } newReader.Close() if !bytes.Equal(newReadBytes, buf) { - logError(testName, function, args, startTime, "", "Bytes mismatch", err) + logError(testName, function, args, startTime, "", "Bytes mismatch on presigned object upload verification", err) + return + } + + function = "Presign(method, bucketName, objectName, expires, reqParams, extraHeaders)" + functionAll += ", " + function + presignExtraHeaders := map[string][]string{ + "mysecret": {"abcxxx"}, + } + args = map[string]interface{}{ + "method": "PUT", + "bucketName": bucketName, + "objectName": objectName + "-presign-custom", + "expires": 3600 * time.Second, + "extraHeaders": presignExtraHeaders, + } + _, err = c.Presign(context.Background(), "PUT", bucketName, objectName+"-presign-custom", 3600*time.Second, nil, presignExtraHeaders) + if err == nil { + logError(testName, function, args, startTime, "", "Presigned with extra headers succeeded", err) return }