diff --git a/pkg/api/acl.go b/pkg/api/acl.go new file mode 100644 index 000000000..da21d36dc --- /dev/null +++ b/pkg/api/acl.go @@ -0,0 +1,72 @@ +/* + * Minimalist Object Storage, (C) 2015 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 api + +import ( + "net/http" + "strings" +) + +// Please read for more information - http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl +// +// Here We are only supporting 'acl's through request headers not through their request body +// http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#setting-acls + +// Minio only supports three types for now i.e 'private, public-read, public-read-write' +type ACLType int + +const ( + unsupportedACLType ACLType = iota + privateACLType + publicReadACLType + publicReadWriteACLType +) + +// Get acl type requested from 'x-amz-acl' header +func getACLType(req *http.Request) ACLType { + aclHeader := req.Header.Get("x-amz-acl") + switch { + case strings.HasPrefix(aclHeader, "private"): + return privateACLType + case strings.HasPrefix(aclHeader, "public-read"): + return publicReadACLType + case strings.HasPrefix(aclHeader, "public-read-write"): + return publicReadWriteACLType + default: + return unsupportedACLType + } +} + +// ACL type to human readable string +func getACLTypeString(acl ACLType) string { + switch acl { + case privateACLType: + { + return "private" + } + case publicReadACLType: + { + return "public-read" + } + case publicReadWriteACLType: + { + return "public-read-write" + } + default: + return "" + } +} diff --git a/pkg/api/api_bucket_handlers.go b/pkg/api/api_bucket_handlers.go index 26a9b29e5..0137bbbbd 100644 --- a/pkg/api/api_bucket_handlers.go +++ b/pkg/api/api_bucket_handlers.go @@ -32,14 +32,19 @@ import ( // criteria to return a subset of the objects in a bucket. // func (server *minioAPI) listObjectsHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) - bucket := vars["bucket"] + acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } resources := getBucketResources(req.URL.Query()) if resources.Maxkeys == 0 { resources.Maxkeys = maxObjectList } - acceptsContentType := getContentType(req) + + vars := mux.Vars(req) + bucket := vars["bucket"] objects, resources, err := server.driver.ListObjects(bucket, resources) switch err.(type) { case nil: // success @@ -49,8 +54,8 @@ func (server *minioAPI) listObjectsHandler(w http.ResponseWriter, req *http.Requ w.WriteHeader(http.StatusOK) // write body response := generateObjectsListResult(bucket, objects, resources) - encodedResponse := encodeResponse(response, acceptsContentType) - w.Write(encodedResponse) + encodedSuccessResponse := encodeSuccessResponse(response, acceptsContentType) + w.Write(encodedSuccessResponse) } case drivers.BucketNotFound: { @@ -78,6 +83,11 @@ func (server *minioAPI) listObjectsHandler(w http.ResponseWriter, req *http.Requ // owned by the authenticated sender of the request. func (server *minioAPI) listBucketsHandler(w http.ResponseWriter, req *http.Request) { acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } + buckets, err := server.driver.ListBuckets() // cannot fallthrough in (type) switch :( switch err := err.(type) { @@ -88,8 +98,8 @@ func (server *minioAPI) listBucketsHandler(w http.ResponseWriter, req *http.Requ setCommonHeaders(w, getContentTypeString(acceptsContentType)) w.WriteHeader(http.StatusOK) // write response - encodedResponse := encodeResponse(response, acceptsContentType) - w.Write(encodedResponse) + encodedSuccessResponse := encodeSuccessResponse(response, acceptsContentType) + w.Write(encodedSuccessResponse) } default: { @@ -103,11 +113,22 @@ func (server *minioAPI) listBucketsHandler(w http.ResponseWriter, req *http.Requ // ---------- // This implementation of the PUT operation creates a new bucket for authenticated request func (server *minioAPI) putBucketHandler(w http.ResponseWriter, req *http.Request) { + acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } + + // read from 'x-amz-acl' + aclType := getACLType(req) + if aclType == unsupportedACLType { + writeErrorResponse(w, req, NotImplemented, acceptsContentType, req.URL.Path) + return + } + vars := mux.Vars(req) bucket := vars["bucket"] err := server.driver.CreateBucket(bucket) - - acceptsContentType := getContentType(req) switch err.(type) { case nil: { @@ -138,10 +159,14 @@ func (server *minioAPI) putBucketHandler(w http.ResponseWriter, req *http.Reques // have permission to access it. Otherwise, the operation might // return responses such as 404 Not Found and 403 Forbidden. func (server *minioAPI) headBucketHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) - bucket := vars["bucket"] acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } + vars := mux.Vars(req) + bucket := vars["bucket"] _, err := server.driver.GetBucketMetadata(bucket) switch err.(type) { case nil: diff --git a/pkg/api/api_generic_handlers.go b/pkg/api/api_generic_handlers.go index a9308c1d3..d60decf4e 100644 --- a/pkg/api/api_generic_handlers.go +++ b/pkg/api/api_generic_handlers.go @@ -142,14 +142,3 @@ func ignoreUnImplementedObjectResources(req *http.Request) bool { } return false } - -func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorType int, acceptsContentType contentType, resource string) { - error := getErrorCode(errorType) - errorResponse := getErrorResponse(error, resource) - // set headers - setCommonHeaders(w, getContentTypeString(acceptsContentType)) - w.WriteHeader(error.HTTPStatusCode) - // write body - encodedErrorResponse := encodeErrorResponse(errorResponse, acceptsContentType) - w.Write(encodedErrorResponse) -} diff --git a/pkg/api/api_object_handlers.go b/pkg/api/api_object_handlers.go index 28a6b24b4..152a645ac 100644 --- a/pkg/api/api_object_handlers.go +++ b/pkg/api/api_object_handlers.go @@ -30,12 +30,16 @@ import ( // This implementation of the GET operation retrieves object. To use GET, // you must have READ access to the object. func (server *minioAPI) getObjectHandler(w http.ResponseWriter, req *http.Request) { - var object, bucket string acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } + + var object, bucket string vars := mux.Vars(req) bucket = vars["bucket"] object = vars["object"] - metadata, err := server.driver.GetObjectMetadata(bucket, object, "") switch err := err.(type) { case nil: // success @@ -95,12 +99,16 @@ func (server *minioAPI) getObjectHandler(w http.ResponseWriter, req *http.Reques // ----------- // The HEAD operation retrieves metadata from an object without returning the object itself. func (server *minioAPI) headObjectHandler(w http.ResponseWriter, req *http.Request) { - var object, bucket string acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } + + var object, bucket string vars := mux.Vars(req) bucket = vars["bucket"] object = vars["object"] - metadata, err := server.driver.GetObjectMetadata(bucket, object, "") switch err := err.(type) { case nil: @@ -128,14 +136,22 @@ func (server *minioAPI) headObjectHandler(w http.ResponseWriter, req *http.Reque // ---------- // This implementation of the PUT operation adds an object to a bucket. func (server *minioAPI) putObjectHandler(w http.ResponseWriter, req *http.Request) { + acceptsContentType := getContentType(req) + if acceptsContentType == unknownContentType { + writeErrorResponse(w, req, NotAcceptable, acceptsContentType, req.URL.Path) + return + } + var object, bucket string vars := mux.Vars(req) - acceptsContentType := getContentType(req) bucket = vars["bucket"] object = vars["object"] - - // get Content-MD5 sent by client + // get Content-MD5 sent by client and verify if valid md5 := req.Header.Get("Content-MD5") + if !isValidMD5(md5) { + writeErrorResponse(w, req, InvalidDigest, acceptsContentType, req.URL.Path) + return + } err := server.driver.CreateObject(bucket, object, "", md5, req.Body) switch err := err.(type) { case nil: diff --git a/pkg/api/api_response.go b/pkg/api/api_response.go index 00a17bae2..a7b2c9065 100644 --- a/pkg/api/api_response.go +++ b/pkg/api/api_response.go @@ -17,6 +17,7 @@ package api import ( + "net/http" "sort" "github.com/minio-io/minio/pkg/storage/drivers" @@ -107,3 +108,14 @@ func generateObjectsListResult(bucket string, objects []drivers.ObjectMetadata, data.CommonPrefixes = prefixes return data } + +func writeErrorResponse(w http.ResponseWriter, req *http.Request, errorType int, acceptsContentType contentType, resource string) { + error := getErrorCode(errorType) + errorResponse := getErrorResponse(error, resource) + // set headers + setCommonHeaders(w, getContentTypeString(acceptsContentType)) + w.WriteHeader(error.HTTPStatusCode) + // write body + encodedErrorResponse := encodeErrorResponse(errorResponse, acceptsContentType) + w.Write(encodedErrorResponse) +} diff --git a/pkg/api/contenttype.go b/pkg/api/contenttype.go index 44921f46e..8cd164d40 100644 --- a/pkg/api/contenttype.go +++ b/pkg/api/contenttype.go @@ -24,7 +24,8 @@ import ( type contentType int const ( - xmlContentType contentType = iota + unknownContentType contentType = iota + xmlContentType jsonContentType ) @@ -34,8 +35,10 @@ func getContentType(req *http.Request) contentType { switch { case strings.HasPrefix(acceptHeader, "application/json"): return jsonContentType - default: + case strings.HasPrefix(acceptHeader, "application/xml"): return xmlContentType + default: + return unknownContentType } } @@ -44,9 +47,12 @@ func getContentTypeString(content contentType) string { switch content { case jsonContentType: { - return "application/json" } + case xmlContentType: + { + return "application/xml" + } default: { return "application/xml" diff --git a/pkg/api/errors.go b/pkg/api/errors.go index dcd7e2b8f..d316be67e 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors.go @@ -38,7 +38,7 @@ type ErrorResponse struct { HostID string } -// Error codes, non exhaustive list +// Error codes, non exhaustive list - http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html const ( AccessDenied = iota BadDigest @@ -63,6 +63,11 @@ const ( TooManyBuckets ) +// Error codes, non exhaustive list - standard HTTP errors +const ( + NotAcceptable = iota + 21 +) + // Error code to Error structure map var errorCodeResponse = map[int]Error{ AccessDenied: { @@ -170,6 +175,12 @@ var errorCodeResponse = map[int]Error{ Description: "You have attempted to create more buckets than allowed.", HTTPStatusCode: http.StatusBadRequest, }, + NotAcceptable: { + Code: "NotAcceptable", + Description: `The requested resource is only capable of generating content + not acceptable according to the Accept headers sent in the request.`, + HTTPStatusCode: http.StatusNotAcceptable, + }, } // errorCodeError provides errorCode to Error. It returns empty if the code provided is unknown diff --git a/pkg/api/headers.go b/pkg/api/headers.go index ac9725f59..3e284f7dd 100644 --- a/pkg/api/headers.go +++ b/pkg/api/headers.go @@ -52,6 +52,9 @@ func encodeErrorResponse(response interface{}, acceptsType contentType) []byte { encoder = xml.NewEncoder(&bytesBuffer) case jsonContentType: encoder = json.NewEncoder(&bytesBuffer) + // by default even if unknown Accept header received handle it by sending XML contenttype response + default: + encoder = xml.NewEncoder(&bytesBuffer) } encoder.Encode(response) return bytesBuffer.Bytes() @@ -77,7 +80,7 @@ func setRangeObjectHeaders(w http.ResponseWriter, metadata drivers.ObjectMetadat w.Header().Set("Content-Range", contentRange.getContentRange()) } -func encodeResponse(response interface{}, acceptsType contentType) []byte { +func encodeSuccessResponse(response interface{}, acceptsType contentType) []byte { var encoder encoder var bytesBuffer bytes.Buffer switch acceptsType { diff --git a/pkg/api/utils.go b/pkg/api/utils.go new file mode 100644 index 000000000..33fc2352e --- /dev/null +++ b/pkg/api/utils.go @@ -0,0 +1,14 @@ +package api + +import ( + "encoding/base64" + "strings" +) + +func isValidMD5(md5 string) bool { + _, err := base64.StdEncoding.DecodeString(strings.TrimSpace(md5)) + if err != nil { + return false + } + return true +}