diff --git a/object-handlers.go b/object-handlers.go index 2195ca76d..8284ea32a 100644 --- a/object-handlers.go +++ b/object-handlers.go @@ -18,6 +18,7 @@ package main import ( "net/http" + "net/url" "strconv" "github.com/gorilla/mux" @@ -29,6 +30,24 @@ const ( maxPartsList = 1000 ) +// supportedGetReqParams - supported request parameters for GET +// presigned request. +var supportedGetReqParams = map[string]string{ + "response-expires": "Expires", + "response-content-type": "Content-Type", + "response-cache-control": "Cache-Control", + "response-content-disposition": "Content-Disposition", +} + +// setResponseHeaders - set any requested parameters as response headers. +func setResponseHeaders(w http.ResponseWriter, reqParams url.Values) { + for k, v := range reqParams { + if header, ok := supportedGetReqParams[k]; ok { + w.Header()[header] = v + } + } +} + // GetObjectHandler - GET Object // ---------- // This implementation of the GET operation retrieves object. To use GET, @@ -69,7 +88,14 @@ func (api CloudStorageAPI) GetObjectHandler(w http.ResponseWriter, req *http.Req writeErrorResponse(w, req, InvalidRange, req.URL.Path) return } + + // Set standard object headers. setObjectHeaders(w, metadata, hrange) + + // Set any additional requested response headers. + setResponseHeaders(w, req.URL.Query()) + + // Get the object. if _, err = api.Filesystem.GetObject(w, bucket, object, hrange.start, hrange.length); err != nil { errorIf(err.Trace(), "GetObject failed.", nil) return diff --git a/pkg/fs/signature.go b/pkg/fs/signature.go index 83959a04f..e17783838 100644 --- a/pkg/fs/signature.go +++ b/pkg/fs/signature.go @@ -308,6 +308,14 @@ func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) { query.Set("X-Amz-Expires", strconv.Itoa(expireSeconds)) query.Set("X-Amz-SignedHeaders", r.getSignedHeaders(r.extractSignedHeaders())) query.Set("X-Amz-Credential", r.AccessKeyID+"/"+r.getScope(t)) + + // Save other headers available in the request parameters. + for k, v := range r.Request.URL.Query() { + if strings.HasPrefix(strings.ToLower(k), "x-amz") { + continue + } + query[k] = v + } encodedQuery := query.Encode() // Verify if date query is same. diff --git a/vendor/github.com/minio/minio-go/CONTRIBUTING.md b/vendor/github.com/minio/minio-go/CONTRIBUTING.md index b4b224eef..3a938e856 100644 --- a/vendor/github.com/minio/minio-go/CONTRIBUTING.md +++ b/vendor/github.com/minio/minio-go/CONTRIBUTING.md @@ -14,7 +14,7 @@ - Have test cases for the new code. If you have questions about how to do it, please ask in your pull request. - Run `go fmt` - Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request. - - Make sure `go test -race ./...` and `go build` completes. + - Make sure `go test -short -race ./...` and `go build` completes. * Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project - `minio-go` project is strictly conformant with Golang style diff --git a/vendor/github.com/minio/minio-go/README.md b/vendor/github.com/minio/minio-go/README.md index 8e4da4317..967824e1f 100644 --- a/vendor/github.com/minio/minio-go/README.md +++ b/vendor/github.com/minio/minio-go/README.md @@ -83,7 +83,7 @@ func main() { * [FGetObject(bucketName, objectName, filePath) error](examples/s3/fgetobject.go) ### Presigned Operations. -* [PresignedGetObject(bucketName, objectName, time.Duration) (string, error)](examples/s3/presignedgetobject.go) +* [PresignedGetObject(bucketName, objectName, time.Duration, url.Values) (string, error)](examples/s3/presignedgetobject.go) * [PresignedPutObject(bucketName, objectName, time.Duration) (string, error)](examples/s3/presignedputobject.go) * [PresignedPostPolicy(NewPostPolicy()) (map[string]string, error)](examples/s3/presignedpostpolicy.go) diff --git a/vendor/github.com/minio/minio-go/api-presigned.go b/vendor/github.com/minio/minio-go/api-presigned.go index 0f350d22e..1a2a34752 100644 --- a/vendor/github.com/minio/minio-go/api-presigned.go +++ b/vendor/github.com/minio/minio-go/api-presigned.go @@ -18,13 +18,26 @@ package minio import ( "errors" + "net/url" "time" ) +// supportedGetReqParams - supported request parameters for GET +// presigned request. +var supportedGetReqParams = map[string]struct{}{ + "response-expires": struct{}{}, + "response-content-type": struct{}{}, + "response-cache-control": struct{}{}, + "response-content-disposition": struct{}{}, +} + // presignURL - Returns a presigned URL for an input 'method'. // Expires maximum is 7days - ie. 604800 and minimum is 1. -func (c Client) presignURL(method string, bucketName string, objectName string, expires time.Duration) (url string, err error) { +func (c Client) presignURL(method string, bucketName string, objectName string, expires time.Duration, reqParams url.Values) (urlStr string, err error) { // Input validation. + if method == "" { + return "", ErrInvalidArgument("method cannot be empty.") + } if err := isValidBucketName(bucketName); err != nil { return "", err } @@ -35,35 +48,50 @@ func (c Client) presignURL(method string, bucketName string, objectName string, return "", err } - if method == "" { - return "", ErrInvalidArgument("method cannot be empty.") - } - + // Convert expires into seconds. expireSeconds := int64(expires / time.Second) - // Instantiate a new request. - // Since expires is set newRequest will presign the request. - req, err := c.newRequest(method, requestMetadata{ + reqMetadata := requestMetadata{ presignURL: true, bucketName: bucketName, objectName: objectName, expires: expireSeconds, - }) + } + + // For "GET" we are handling additional request parameters to + // override its response headers. + if method == "GET" { + // Verify if input map has unsupported params, if yes exit. + for k := range reqParams { + if _, ok := supportedGetReqParams[k]; !ok { + return "", ErrInvalidArgument(k + " unsupported request parameter for presigned GET.") + } + } + // Save the request parameters to be used in presigning for + // GET request. + reqMetadata.queryValues = reqParams + } + + // Instantiate a new request. + // Since expires is set newRequest will presign the request. + req, err := c.newRequest(method, reqMetadata) if err != nil { return "", err } return req.URL.String(), nil } -// PresignedGetObject - Returns a presigned URL to access an object without credentials. -// Expires maximum is 7days - ie. 604800 and minimum is 1. -func (c Client) PresignedGetObject(bucketName string, objectName string, expires time.Duration) (url string, err error) { - return c.presignURL("GET", bucketName, objectName, expires) +// PresignedGetObject - Returns a presigned URL to access an object +// without credentials. Expires maximum is 7days - ie. 604800 and +// minimum is 1. Additionally you can override a set of response +// headers using the query parameters. +func (c Client) PresignedGetObject(bucketName string, objectName string, expires time.Duration, reqParams url.Values) (url string, err error) { + return c.presignURL("GET", bucketName, objectName, expires, reqParams) } // PresignedPutObject - Returns a presigned URL to upload an object without credentials. // Expires maximum is 7days - ie. 604800 and minimum is 1. func (c Client) PresignedPutObject(bucketName string, objectName string, expires time.Duration) (url string, err error) { - return c.presignURL("PUT", bucketName, objectName, expires) + return c.presignURL("PUT", bucketName, objectName, expires, nil) } // PresignedPostPolicy - Returns POST form data to upload an object at a location. diff --git a/vendor/github.com/minio/minio-go/api-put-object-common.go b/vendor/github.com/minio/minio-go/api-put-object-common.go index 1584497bb..7c73b24c3 100644 --- a/vendor/github.com/minio/minio-go/api-put-object-common.go +++ b/vendor/github.com/minio/minio-go/api-put-object-common.go @@ -55,7 +55,7 @@ func shouldUploadPart(objPart objectPart, objectParts map[int]objectPart) bool { return true } // if md5sum mismatches should upload the part. - if objPart.ETag == uploadedPart.ETag { + if objPart.ETag != uploadedPart.ETag { return true } return false diff --git a/vendor/github.com/minio/minio-go/api.go b/vendor/github.com/minio/minio-go/api.go index f4d2f52f7..d52d641bb 100644 --- a/vendor/github.com/minio/minio-go/api.go +++ b/vendor/github.com/minio/minio-go/api.go @@ -325,6 +325,12 @@ func (c Client) do(req *http.Request) (*http.Response, error) { // execute the request. resp, err := c.httpClient.Do(req) if err != nil { + // Handle this specifically for now until future Golang + // versions fix this issue properly. + urlErr, ok := err.(*url.Error) + if ok && strings.Contains(urlErr.Err.Error(), "EOF") { + return nil, fmt.Errorf("Connection closed by foreign host %s. Retry again.", urlErr.URL) + } return resp, err } // If trace is enabled, dump http request and response. @@ -516,7 +522,7 @@ type CloudStorageClient interface { PutObjectWithProgress(bucketName, objectName string, reader io.Reader, contentType string, progress io.Reader) (n int64, err error) // Presigned operations. - PresignedGetObject(bucketName, objectName string, expires time.Duration) (presignedURL string, err error) + PresignedGetObject(bucketName, objectName string, expires time.Duration, reqParams url.Values) (presignedURL string, err error) PresignedPutObject(bucketName, objectName string, expires time.Duration) (presignedURL string, err error) PresignedPostPolicy(*PostPolicy) (formData map[string]string, err error) diff --git a/vendor/github.com/minio/minio-go/api_functional_v2_test.go b/vendor/github.com/minio/minio-go/api_functional_v2_test.go index 990e02810..a3f3411a5 100644 --- a/vendor/github.com/minio/minio-go/api_functional_v2_test.go +++ b/vendor/github.com/minio/minio-go/api_functional_v2_test.go @@ -24,6 +24,7 @@ import ( "io/ioutil" "math/rand" "net/http" + "net/url" "os" "testing" "time" @@ -954,11 +955,12 @@ func TestFunctionalV2(t *testing.T) { t.Fatal("Error: ", err) } - presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second) + // Generate presigned GET object url. + presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second, nil) if err != nil { t.Fatal("Error: ", err) } - + // Verify if presigned url works. resp, err := http.Get(presignedGetURL) if err != nil { t.Fatal("Error: ", err) @@ -974,6 +976,34 @@ func TestFunctionalV2(t *testing.T) { t.Fatal("Error: bytes mismatch.") } + // Set request parameters. + reqParams := make(url.Values) + reqParams.Set("response-content-disposition", "attachment; filename=\"test.txt\"") + // Generate presigned GET object url. + presignedGetURL, err = c.PresignedGetObject(bucketName, objectName, 3600*time.Second, reqParams) + if err != nil { + t.Fatal("Error: ", err) + } + // Verify if presigned url works. + resp, err = http.Get(presignedGetURL) + if err != nil { + t.Fatal("Error: ", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatal("Error: ", resp.Status) + } + newPresignedBytes, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Error: ", err) + } + if !bytes.Equal(newPresignedBytes, buf) { + t.Fatal("Error: bytes mismatch for presigned GET url.") + } + // Verify content disposition. + if resp.Header.Get("Content-Disposition") != "attachment; filename=\"test.txt\"" { + t.Fatalf("Error: wrong Content-Disposition received %s", resp.Header.Get("Content-Disposition")) + } + presignedPutURL, err := c.PresignedPutObject(bucketName, objectName+"-presigned", 3600*time.Second) if err != nil { t.Fatal("Error: ", err) diff --git a/vendor/github.com/minio/minio-go/api_functional_v4_test.go b/vendor/github.com/minio/minio-go/api_functional_v4_test.go index 5e88c6124..583ab79b9 100644 --- a/vendor/github.com/minio/minio-go/api_functional_v4_test.go +++ b/vendor/github.com/minio/minio-go/api_functional_v4_test.go @@ -24,6 +24,7 @@ import ( "io/ioutil" "math/rand" "net/http" + "net/url" "os" "testing" "time" @@ -983,11 +984,13 @@ func TestFunctional(t *testing.T) { t.Fatal("Error: ", err) } - presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second) + // Generate presigned GET object url. + presignedGetURL, err := c.PresignedGetObject(bucketName, objectName, 3600*time.Second, nil) if err != nil { t.Fatal("Error: ", err) } + // Verify if presigned url works. resp, err := http.Get(presignedGetURL) if err != nil { t.Fatal("Error: ", err) @@ -1003,6 +1006,32 @@ func TestFunctional(t *testing.T) { t.Fatal("Error: bytes mismatch.") } + // Set request parameters. + reqParams := make(url.Values) + reqParams.Set("response-content-disposition", "attachment; filename=\"test.txt\"") + presignedGetURL, err = c.PresignedGetObject(bucketName, objectName, 3600*time.Second, reqParams) + if err != nil { + t.Fatal("Error: ", err) + } + // Verify if presigned url works. + resp, err = http.Get(presignedGetURL) + if err != nil { + t.Fatal("Error: ", err) + } + if resp.StatusCode != http.StatusOK { + t.Fatal("Error: ", resp.Status) + } + newPresignedBytes, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Error: ", err) + } + if !bytes.Equal(newPresignedBytes, buf) { + t.Fatal("Error: bytes mismatch for presigned GET URL.") + } + if resp.Header.Get("Content-Disposition") != "attachment; filename=\"test.txt\"" { + t.Fatalf("Error: wrong Content-Disposition received %s", resp.Header.Get("Content-Disposition")) + } + presignedPutURL, err := c.PresignedPutObject(bucketName, objectName+"-presigned", 3600*time.Second) if err != nil { t.Fatal("Error: ", err) diff --git a/vendor/github.com/minio/minio-go/request-signature-v2.go b/vendor/github.com/minio/minio-go/request-signature-v2.go index aa0fc9f91..38224557e 100644 --- a/vendor/github.com/minio/minio-go/request-signature-v2.go +++ b/vendor/github.com/minio/minio-go/request-signature-v2.go @@ -73,6 +73,11 @@ func preSignV2(req http.Request, accessKeyID, secretAccessKey string, expires in // Get encoded URL path. path := encodeURL2Path(req.URL) + if len(req.URL.Query()) > 0 { + // Keep the usual queries unescaped for string to sign. + query, _ := url.QueryUnescape(queryEncode(req.URL.Query())) + path = path + "?" + query + } // Find epoch expires when the request will expire. epochExpires := d.Unix() + expires @@ -93,12 +98,16 @@ func preSignV2(req http.Request, accessKeyID, secretAccessKey string, expires in query.Set("AWSAccessKeyId", accessKeyID) } - // Fill in Expires and Signature for presigned query. + // Fill in Expires for presigned query. query.Set("Expires", strconv.FormatInt(epochExpires, 10)) - query.Set("Signature", signature) // Encode query and save. - req.URL.RawQuery = query.Encode() + req.URL.RawQuery = queryEncode(query) + + // Save signature finally. + req.URL.RawQuery += "&Signature=" + urlEncodePath(signature) + + // Return. return &req } @@ -283,7 +292,7 @@ func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request) { // Request parameters if len(vv[0]) > 0 { buf.WriteByte('=') - buf.WriteString(url.QueryEscape(vv[0])) + buf.WriteString(strings.Replace(url.QueryEscape(vv[0]), "+", "%20", -1)) } } } diff --git a/vendor/vendor.json b/vendor/vendor.json index 61b9d767f..2339b35e0 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -59,8 +59,8 @@ }, { "path": "github.com/minio/minio-go", - "revision": "c5884ce9ce3ac73b025d0bc58c4d3d72870edc0b", - "revisionTime": "2016-02-02T13:13:10+05:30" + "revision": "280f16a52008d3ebba1bd64398b9b082e6738386", + "revisionTime": "2016-02-07T03:45:25-08:00" }, { "path": "github.com/minio/minio-xl/pkg/atomic", diff --git a/web-handlers.go b/web-handlers.go index 169bedd7b..df8a693f2 100644 --- a/web-handlers.go +++ b/web-handlers.go @@ -20,7 +20,9 @@ import ( "fmt" "net" "net/http" + "net/url" "os" + "path/filepath" "runtime" "strconv" "strings" @@ -206,7 +208,10 @@ func (web *WebAPI) GetObjectURL(r *http.Request, args *GetObjectURLArgs, reply * if e != nil { return e } - signedURLStr, e := client.PresignedGetObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second) + reqParams := make(url.Values) + // Set content disposition for browser to download the file. + reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(args.ObjectName))) + signedURLStr, e := client.PresignedGetObject(args.BucketName, args.ObjectName, time.Duration(60*60)*time.Second, reqParams) if e != nil { return e }