diff --git a/api-signature.go b/api-signature.go deleted file mode 100644 index c178667b0..000000000 --- a/api-signature.go +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Minio Cloud 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 main - -import ( - "bytes" - "encoding/base64" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "strings" - "time" - - "github.com/minio/minio/pkg/probe" - v4 "github.com/minio/minio/pkg/signature" -) - -const ( - authHeaderPrefix = "AWS4-HMAC-SHA256" - iso8601Format = "20060102T150405Z" -) - -// getCredentialsFromAuth parse credentials tag from authorization value -func getCredentialsFromAuth(authValue string) ([]string, *probe.Error) { - if authValue == "" { - return nil, probe.NewError(errMissingAuthHeaderValue) - } - // replace all spaced strings - authValue = strings.Replace(authValue, " ", "", -1) - if !strings.HasPrefix(authValue, authHeaderPrefix) { - return nil, probe.NewError(errMissingFieldsAuthHeader) - } - if !strings.HasPrefix(strings.TrimPrefix(authValue, authHeaderPrefix), "Credential") { - return nil, probe.NewError(errInvalidAuthHeaderPrefix) - } - authValue = strings.TrimPrefix(authValue, authHeaderPrefix) - authFields := strings.Split(strings.TrimSpace(authValue), ",") - if len(authFields) != 3 { - return nil, probe.NewError(errInvalidAuthHeaderValue) - } - credentials := strings.Split(strings.TrimSpace(authFields[0]), "=") - if len(credentials) != 2 { - return nil, probe.NewError(errMissingFieldsCredentialTag) - } - credentialElements := strings.Split(strings.TrimSpace(credentials[1]), "/") - if len(credentialElements) != 5 { - return nil, probe.NewError(errCredentialTagMalformed) - } - return credentialElements, nil -} - -func getSignatureFromAuth(authHeaderValue string) (string, *probe.Error) { - authValue := strings.TrimPrefix(authHeaderValue, authHeaderPrefix) - authFields := strings.Split(strings.TrimSpace(authValue), ",") - if len(authFields) != 3 { - return "", probe.NewError(errInvalidAuthHeaderValue) - } - if len(strings.Split(strings.TrimSpace(authFields[2]), "=")) != 2 { - return "", probe.NewError(errMissingFieldsSignatureTag) - } - signature := strings.Split(strings.TrimSpace(authFields[2]), "=")[1] - return signature, nil -} - -func getSignedHeadersFromAuth(authHeaderValue string) ([]string, *probe.Error) { - authValue := strings.TrimPrefix(authHeaderValue, authHeaderPrefix) - authFields := strings.Split(strings.TrimSpace(authValue), ",") - if len(authFields) != 3 { - return nil, probe.NewError(errInvalidAuthHeaderValue) - } - if len(strings.Split(strings.TrimSpace(authFields[1]), "=")) != 2 { - return nil, probe.NewError(errMissingFieldsSignedHeadersTag) - } - signedHeaders := strings.Split(strings.Split(strings.TrimSpace(authFields[1]), "=")[1], ";") - return signedHeaders, nil -} - -// verify if region value is valid with configured minioRegion. -func isValidRegion(region string, minioRegion string) *probe.Error { - if minioRegion == "" { - minioRegion = "us-east-1" - } - if region != minioRegion && region != "US" { - return probe.NewError(errInvalidRegion) - } - return nil -} - -// stripRegion - strip only region from auth header. -func stripRegion(authHeaderValue string) (string, *probe.Error) { - credentialElements, err := getCredentialsFromAuth(authHeaderValue) - if err != nil { - return "", err.Trace(authHeaderValue) - } - region := credentialElements[2] - return region, nil -} - -// stripAccessKeyID - strip only access key id from auth header. -func stripAccessKeyID(authHeaderValue string) (string, *probe.Error) { - credentialElements, err := getCredentialsFromAuth(authHeaderValue) - if err != nil { - return "", err.Trace() - } - accessKeyID := credentialElements[0] - if !isValidAccessKey(accessKeyID) { - return "", probe.NewError(errAccessKeyIDInvalid) - } - return accessKeyID, nil -} - -// initSignatureV4 initializing signature verification. -func initSignatureV4(req *http.Request) (*v4.Signature, *probe.Error) { - // strip auth from authorization header. - authHeaderValue := req.Header.Get("Authorization") - - config, err := loadConfigV2() - if err != nil { - return nil, err.Trace() - } - - region, err := stripRegion(authHeaderValue) - if err != nil { - return nil, err.Trace(authHeaderValue) - } - - if err = isValidRegion(region, config.Credentials.Region); err != nil { - return nil, err.Trace(authHeaderValue) - } - - accessKeyID, err := stripAccessKeyID(authHeaderValue) - if err != nil { - return nil, err.Trace(authHeaderValue) - } - signature, err := getSignatureFromAuth(authHeaderValue) - if err != nil { - return nil, err.Trace(authHeaderValue) - } - signedHeaders, err := getSignedHeadersFromAuth(authHeaderValue) - if err != nil { - return nil, err.Trace(authHeaderValue) - } - if config.Credentials.AccessKeyID == accessKeyID { - signature := &v4.Signature{ - AccessKeyID: config.Credentials.AccessKeyID, - SecretAccessKey: config.Credentials.SecretAccessKey, - Region: region, - Signature: signature, - SignedHeaders: signedHeaders, - Request: req, - } - return signature, nil - } - return nil, probe.NewError(errAccessKeyIDInvalid) -} - -func extractHTTPFormValues(reader *multipart.Reader) (io.Reader, map[string]string, *probe.Error) { - /// HTML Form values - formValues := make(map[string]string) - filePart := new(bytes.Buffer) - var e error - for e == nil { - var part *multipart.Part - part, e = reader.NextPart() - if part != nil { - if part.FileName() == "" { - buffer, e := ioutil.ReadAll(part) - if e != nil { - return nil, nil, probe.NewError(e) - } - formValues[http.CanonicalHeaderKey(part.FormName())] = string(buffer) - } else { - if _, e := io.Copy(filePart, part); e != nil { - return nil, nil, probe.NewError(e) - } - } - } - } - return filePart, formValues, nil -} - -func applyPolicy(formValues map[string]string) *probe.Error { - if formValues["X-Amz-Algorithm"] != "AWS4-HMAC-SHA256" { - return probe.NewError(errUnsupportedAlgorithm) - } - /// Decoding policy - policyBytes, e := base64.StdEncoding.DecodeString(formValues["Policy"]) - if e != nil { - return probe.NewError(e) - } - postPolicyForm, err := v4.ParsePostPolicyForm(string(policyBytes)) - if err != nil { - return err.Trace() - } - if !postPolicyForm.Expiration.After(time.Now().UTC()) { - return probe.NewError(errPolicyAlreadyExpired) - } - if postPolicyForm.Conditions.Policies["$bucket"].Operator == "eq" { - if formValues["Bucket"] != postPolicyForm.Conditions.Policies["$bucket"].Value { - return probe.NewError(errPolicyMissingFields) - } - } - if postPolicyForm.Conditions.Policies["$x-amz-date"].Operator == "eq" { - if formValues["X-Amz-Date"] != postPolicyForm.Conditions.Policies["$x-amz-date"].Value { - return probe.NewError(errPolicyMissingFields) - } - } - if postPolicyForm.Conditions.Policies["$Content-Type"].Operator == "starts-with" { - if !strings.HasPrefix(formValues["Content-Type"], postPolicyForm.Conditions.Policies["$Content-Type"].Value) { - return probe.NewError(errPolicyMissingFields) - } - } - if postPolicyForm.Conditions.Policies["$Content-Type"].Operator == "eq" { - if formValues["Content-Type"] != postPolicyForm.Conditions.Policies["$Content-Type"].Value { - return probe.NewError(errPolicyMissingFields) - } - } - if postPolicyForm.Conditions.Policies["$key"].Operator == "starts-with" { - if !strings.HasPrefix(formValues["Key"], postPolicyForm.Conditions.Policies["$key"].Value) { - return probe.NewError(errPolicyMissingFields) - } - } - if postPolicyForm.Conditions.Policies["$key"].Operator == "eq" { - if formValues["Key"] != postPolicyForm.Conditions.Policies["$key"].Value { - return probe.NewError(errPolicyMissingFields) - } - } - return nil -} - -// initPostPresignedPolicyV4 initializing post policy signature verification -func initPostPresignedPolicyV4(formValues map[string]string) (*v4.Signature, *probe.Error) { - credentialElements := strings.Split(strings.TrimSpace(formValues["X-Amz-Credential"]), "/") - if len(credentialElements) != 5 { - return nil, probe.NewError(errCredentialTagMalformed) - } - accessKeyID := credentialElements[0] - if !isValidAccessKey(accessKeyID) { - return nil, probe.NewError(errAccessKeyIDInvalid) - } - config, err := loadConfigV2() - if err != nil { - return nil, err.Trace() - } - region := credentialElements[2] - if config.Credentials.AccessKeyID == accessKeyID { - signature := &v4.Signature{ - AccessKeyID: config.Credentials.AccessKeyID, - SecretAccessKey: config.Credentials.SecretAccessKey, - Region: region, - Signature: formValues["X-Amz-Signature"], - PresignedPolicy: formValues["Policy"], - } - return signature, nil - } - return nil, probe.NewError(errAccessKeyIDInvalid) -} - -// initPresignedSignatureV4 initializing presigned signature verification -func initPresignedSignatureV4(req *http.Request) (*v4.Signature, *probe.Error) { - credentialElements := strings.Split(strings.TrimSpace(req.URL.Query().Get("X-Amz-Credential")), "/") - if len(credentialElements) != 5 { - return nil, probe.NewError(errCredentialTagMalformed) - } - accessKeyID := credentialElements[0] - if !isValidAccessKey(accessKeyID) { - return nil, probe.NewError(errAccessKeyIDInvalid) - } - config, err := loadConfigV2() - if err != nil { - return nil, err.Trace() - } - region := credentialElements[2] - signedHeaders := strings.Split(strings.TrimSpace(req.URL.Query().Get("X-Amz-SignedHeaders")), ";") - signature := strings.TrimSpace(req.URL.Query().Get("X-Amz-Signature")) - if config.Credentials.AccessKeyID == accessKeyID { - signature := &v4.Signature{ - AccessKeyID: config.Credentials.AccessKeyID, - SecretAccessKey: config.Credentials.SecretAccessKey, - Region: region, - Signature: signature, - SignedHeaders: signedHeaders, - Presigned: true, - Request: req, - } - return signature, nil - } - return nil, probe.NewError(errAccessKeyIDInvalid) -} diff --git a/auth-handler.go b/auth-handler.go new file mode 100644 index 000000000..295431256 --- /dev/null +++ b/auth-handler.go @@ -0,0 +1,68 @@ +/* + * Minio Cloud 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 main + +import ( + "fmt" + "net/http" + + jwtgo "github.com/dgrijalva/jwt-go" +) + +const ( + signV4Algorithm = "AWS4-HMAC-SHA256" + jwtAlgorithm = "Bearer" +) + +// authHandler - handles all the incoming authorization headers and +// validates them if possible. +type authHandler struct { + handler http.Handler +} + +// setAuthHandler to validate authorization header for the incoming request. +func setAuthHandler(h http.Handler) http.Handler { + return authHandler{h} +} + +func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Verify if request has post policy signature. + if isRequestPostPolicySignatureV4(r) && r.Method == "POST" { + a.handler.ServeHTTP(w, r) + return + } + + // Verify JWT authorization header is present. + if isRequestJWT(r) { + // Validate Authorization header to be valid. + jwt := InitJWT() + token, e := jwtgo.ParseFromRequest(r, func(token *jwtgo.Token) (interface{}, error) { + if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return jwt.secretAccessKey, nil + }) + if e != nil || !token.Valid { + w.WriteHeader(http.StatusUnauthorized) + return + } + } + + // For signed, presigned, jwt and anonymous requests let the top level + // caller handle and verify. + a.handler.ServeHTTP(w, r) +} diff --git a/bucket-handlers.go b/bucket-handlers.go index ddebb22c0..43e16618d 100644 --- a/bucket-handlers.go +++ b/bucket-handlers.go @@ -17,26 +17,34 @@ package main import ( + "bytes" "encoding/hex" + "io" "io/ioutil" + "mime/multipart" "net/http" "github.com/gorilla/mux" "github.com/minio/minio/pkg/crypto/sha256" "github.com/minio/minio/pkg/fs" "github.com/minio/minio/pkg/probe" - v4 "github.com/minio/minio/pkg/signature" + signV4 "github.com/minio/minio/pkg/signature" ) // GetBucketLocationHandler - GET Bucket location. // ------------------------- // This operation returns bucket location. -func (api CloudStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) + return + } + + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } @@ -45,20 +53,23 @@ func (api CloudStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, req * errorIf(err.Trace(), "GetBucketMetadata failed.", nil) switch err.ToGoError().(type) { case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } - // TODO: Location value for LocationResponse is deliberately not used, until - // we bring in a mechanism of configurable regions. For the time being - // default region is empty i.e 'us-east-1'. - encodedSuccessResponse := encodeSuccessResponse(LocationResponse{}) // generate response - setCommonHeaders(w) // write headers + // Generate response. + encodedSuccessResponse := encodeSuccessResponse(LocationResponse{}) + if api.Region != "us-east-1" { + encodedSuccessResponse = encodeSuccessResponse(LocationResponse{ + Location: api.Region, + }) + } + setCommonHeaders(w) // write headers. writeSuccessResponse(w, encodedSuccessResponse) } @@ -70,18 +81,23 @@ func (api CloudStorageAPI) GetBucketLocationHandler(w http.ResponseWriter, req * // completed or aborted. This operation returns at most 1,000 multipart // uploads in the response. // -func (api CloudStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) + return + } + + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } - resources := getBucketMultipartResources(req.URL.Query()) + resources := getBucketMultipartResources(r.URL.Query()) if resources.MaxUploads < 0 { - writeErrorResponse(w, req, InvalidMaxUploads, req.URL.Path) + writeErrorResponse(w, r, InvalidMaxUploads, r.URL.Path) return } if resources.MaxUploads == 0 { @@ -93,9 +109,9 @@ func (api CloudStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, re errorIf(err.Trace(), "ListMultipartUploads failed.", nil) switch err.ToGoError().(type) { case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -109,26 +125,31 @@ func (api CloudStorageAPI) ListMultipartUploadsHandler(w http.ResponseWriter, re } // ListObjectsHandler - GET Bucket (List Objects) -// ------------------------- +// -- ----------------------- // This implementation of the GET operation returns some or all (up to 1000) // of the objects in a bucket. You can use the request parameters as selection // criteria to return a subset of the objects in a bucket. // -func (api CloudStorageAPI) ListObjectsHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) ListObjectsHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + // TODO handle encoding type. - prefix, marker, delimiter, maxkeys, _ := getBucketResources(req.URL.Query()) + prefix, marker, delimiter, maxkeys, _ := getBucketResources(r.URL.Query()) if maxkeys < 0 { - writeErrorResponse(w, req, InvalidMaxKeys, req.URL.Path) + writeErrorResponse(w, r, InvalidMaxKeys, r.URL.Path) return } if maxkeys == 0 { @@ -148,16 +169,16 @@ func (api CloudStorageAPI) ListObjectsHandler(w http.ResponseWriter, req *http.R } switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) default: errorIf(err.Trace(), "ListObjects failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } } @@ -165,9 +186,14 @@ func (api CloudStorageAPI) ListObjectsHandler(w http.ResponseWriter, req *http.R // ----------- // This implementation of the GET operation returns a list of all buckets // owned by the authenticated sender of the request. -func (api CloudStorageAPI) ListBucketsHandler(w http.ResponseWriter, req *http.Request) { - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) +func (api CloudStorageAPI) ListBucketsHandler(w http.ResponseWriter, r *http.Request) { + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) + return + } + + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } @@ -183,90 +209,73 @@ func (api CloudStorageAPI) ListBucketsHandler(w http.ResponseWriter, req *http.R return } errorIf(err.Trace(), "ListBuckets failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } // PutBucketHandler - PUT Bucket // ---------- // This implementation of the PUT operation creates a new bucket for authenticated request -func (api CloudStorageAPI) PutBucketHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) PutBucketHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } // read from 'x-amz-acl' - aclType := getACLType(req) + aclType := getACLType(r) if aclType == unsupportedACLType { - writeErrorResponse(w, req, NotImplemented, req.URL.Path) + writeErrorResponse(w, r, NotImplemented, r.URL.Path) return } - var signature *v4.Signature - // Init signature V4 verification - if isRequestSignatureV4(req) { - var err *probe.Error - signature, err = initSignatureV4(req) - if err != nil { - switch err.ToGoError() { - case errInvalidRegion: - errorIf(err.Trace(), "Unknown region in authorization header.", nil) - writeErrorResponse(w, req, AuthorizationHeaderMalformed, req.URL.Path) - return - case errAccessKeyIDInvalid: - errorIf(err.Trace(), "Invalid access key id.", nil) - writeErrorResponse(w, req, InvalidAccessKeyID, req.URL.Path) - return - default: - errorIf(err.Trace(), "Initializing signature v4 failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) - return - } + // if body of request is non-nil then check for validity of Content-Length + if r.Body != nil { + /// if Content-Length is unknown/missing, deny the request + if r.ContentLength == -1 && !contains(r.TransferEncoding, "chunked") { + writeErrorResponse(w, r, MissingContentLength, r.URL.Path) + return } } - // if body of request is non-nil then check for validity of Content-Length - if req.Body != nil { - /// if Content-Length is unknown/missing, deny the request - if req.ContentLength == -1 && !contains(req.TransferEncoding, "chunked") { - writeErrorResponse(w, req, MissingContentLength, req.URL.Path) + // Set http request for signature. + api.Signature.SetHTTPRequestToVerify(r) + + // Verify signature for the incoming body if any. + if api.Signature != nil { + locationBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + errorIf(probe.NewError(e), "MakeBucket failed.", nil) + writeErrorResponse(w, r, InternalError, r.URL.Path) return } - if signature != nil { - locationBytes, e := ioutil.ReadAll(req.Body) - if e != nil { - errorIf(probe.NewError(e), "MakeBucket failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) - return - } - sh := sha256.New() - sh.Write(locationBytes) - ok, err := signature.DoesSignatureMatch(hex.EncodeToString(sh.Sum(nil))) - if err != nil { - errorIf(err.Trace(), "MakeBucket failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) - return - } - if !ok { - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) - return - } + sh := sha256.New() + sh.Write(locationBytes) + ok, err := api.Signature.DoesSignatureMatch(hex.EncodeToString(sh.Sum(nil))) + if err != nil { + errorIf(err.Trace(), "MakeBucket failed.", nil) + writeErrorResponse(w, r, InternalError, r.URL.Path) + return + } + if !ok { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return } } + // Make bucket. err := api.Filesystem.MakeBucket(bucket, getACLTypeString(aclType)) if err != nil { errorIf(err.Trace(), "MakeBucket failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketExists: - writeErrorResponse(w, req, BucketAlreadyExists, req.URL.Path) + writeErrorResponse(w, r, BucketAlreadyExists, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -275,16 +284,41 @@ func (api CloudStorageAPI) PutBucketHandler(w http.ResponseWriter, req *http.Req writeSuccessResponse(w, nil) } +func extractHTTPFormValues(reader *multipart.Reader) (io.Reader, map[string]string, *probe.Error) { + /// HTML Form values + formValues := make(map[string]string) + filePart := new(bytes.Buffer) + var e error + for e == nil { + var part *multipart.Part + part, e = reader.NextPart() + if part != nil { + if part.FileName() == "" { + buffer, e := ioutil.ReadAll(part) + if e != nil { + return nil, nil, probe.NewError(e) + } + formValues[http.CanonicalHeaderKey(part.FormName())] = string(buffer) + } else { + if _, e := io.Copy(filePart, part); e != nil { + return nil, nil, probe.NewError(e) + } + } + } + } + return filePart, formValues, nil +} + // PostPolicyBucketHandler - POST policy // ---------- // This implementation of the POST operation handles object creation with a specified // signature policy in multipart/form-data -func (api CloudStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, req *http.Request) { +func (api CloudStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) { // if body of request is non-nil then check for validity of Content-Length - if req.Body != nil { + if r.Body != nil { /// if Content-Length is unknown/missing, deny the request - if req.ContentLength == -1 { - writeErrorResponse(w, req, MissingContentLength, req.URL.Path) + if r.ContentLength == -1 { + writeErrorResponse(w, r, MissingContentLength, r.URL.Path) return } } @@ -292,65 +326,61 @@ func (api CloudStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, req *h // Here the parameter is the size of the form data that should // be loaded in memory, the remaining being put in temporary // files - reader, e := req.MultipartReader() + reader, e := r.MultipartReader() if e != nil { errorIf(probe.NewError(e), "Unable to initialize multipart reader.", nil) - writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path) + writeErrorResponse(w, r, MalformedPOSTRequest, r.URL.Path) return } fileBody, formValues, err := extractHTTPFormValues(reader) if err != nil { errorIf(err.Trace(), "Unable to parse form values.", nil) - writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path) + writeErrorResponse(w, r, MalformedPOSTRequest, r.URL.Path) return } - bucket := mux.Vars(req)["bucket"] + bucket := mux.Vars(r)["bucket"] formValues["Bucket"] = bucket object := formValues["Key"] - signature, err := initPostPresignedPolicyV4(formValues) - if err != nil { - errorIf(err.Trace(), "Unable to initialize post policy presigned.", nil) - writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path) - return - } var ok bool - if ok, err = signature.DoesPolicySignatureMatch(formValues["X-Amz-Date"]); err != nil { + + // Set http request for signature. + api.Signature.SetHTTPRequestToVerify(r) + + // Verify policy signature. + ok, err = api.Signature.DoesPolicySignatureMatch(formValues) + if err != nil { errorIf(err.Trace(), "Unable to verify signature.", nil) - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } - if ok == false { - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) + if !ok { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } - if err = applyPolicy(formValues); err != nil { + if err = signV4.ApplyPolicyCond(formValues); err != nil { errorIf(err.Trace(), "Invalid request, policy doesn't match with the endpoint.", nil) - writeErrorResponse(w, req, MalformedPOSTRequest, req.URL.Path) + writeErrorResponse(w, r, MalformedPOSTRequest, r.URL.Path) return } - metadata, err := api.Filesystem.CreateObject(bucket, object, "", 0, fileBody, nil) + metadata, err := api.Filesystem.CreateObject(bucket, object, "", -1, fileBody, nil) if err != nil { errorIf(err.Trace(), "CreateObject failed.", nil) switch err.ToGoError().(type) { case fs.RootPathFull: - writeErrorResponse(w, req, RootPathFull, req.URL.Path) + writeErrorResponse(w, r, RootPathFull, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BadDigest: - writeErrorResponse(w, req, BadDigest, req.URL.Path) - case v4.SigDoesNotMatch: - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) + writeErrorResponse(w, r, BadDigest, r.URL.Path) case fs.IncompleteBody: - writeErrorResponse(w, req, IncompleteBody, req.URL.Path) - case fs.EntityTooLarge: - writeErrorResponse(w, req, EntityTooLarge, req.URL.Path) + writeErrorResponse(w, r, IncompleteBody, r.URL.Path) case fs.InvalidDigest: - writeErrorResponse(w, req, InvalidDigest, req.URL.Path) + writeErrorResponse(w, r, InvalidDigest, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -363,19 +393,24 @@ func (api CloudStorageAPI) PostPolicyBucketHandler(w http.ResponseWriter, req *h // PutBucketACLHandler - PUT Bucket ACL // ---------- // This implementation of the PUT operation modifies the bucketACL for authenticated request -func (api CloudStorageAPI) PutBucketACLHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) + return + } + + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } // read from 'x-amz-acl' - aclType := getACLType(req) + aclType := getACLType(r) if aclType == unsupportedACLType { - writeErrorResponse(w, req, NotImplemented, req.URL.Path) + writeErrorResponse(w, r, NotImplemented, r.URL.Path) return } err := api.Filesystem.SetBucketMetadata(bucket, map[string]string{"acl": getACLTypeString(aclType)}) @@ -383,11 +418,11 @@ func (api CloudStorageAPI) PutBucketACLHandler(w http.ResponseWriter, req *http. errorIf(err.Trace(), "PutBucketACL failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -400,12 +435,17 @@ func (api CloudStorageAPI) PutBucketACLHandler(w http.ResponseWriter, req *http. // of a bucket. One must have permission to access the bucket to // know its ``acl``. This operation willl return response of 404 // if bucket not found and 403 for invalid credentials. -func (api CloudStorageAPI) GetBucketACLHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) + return + } + + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } @@ -414,11 +454,11 @@ func (api CloudStorageAPI) GetBucketACLHandler(w http.ResponseWriter, req *http. errorIf(err.Trace(), "GetBucketMetadata failed.", nil) switch err.ToGoError().(type) { case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -437,27 +477,32 @@ func (api CloudStorageAPI) GetBucketACLHandler(w http.ResponseWriter, req *http. // The operation returns a 200 OK if the bucket exists and you // have permission to access it. Otherwise, the operation might // return responses such as 404 Not Found and 403 Forbidden. -func (api CloudStorageAPI) HeadBucketHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) HeadBucketHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + _, err := api.Filesystem.GetBucketMetadata(bucket) if err != nil { errorIf(err.Trace(), "GetBucketMetadata failed.", nil) switch err.ToGoError().(type) { case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -465,12 +510,17 @@ func (api CloudStorageAPI) HeadBucketHandler(w http.ResponseWriter, req *http.Re } // DeleteBucketHandler - Delete bucket -func (api CloudStorageAPI) DeleteBucketHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] - if isRequestRequiresACLCheck(req) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + if isRequestRequiresACLCheck(r) { + writeErrorResponse(w, r, AccessDenied, r.URL.Path) + return + } + + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) return } @@ -479,11 +529,11 @@ func (api CloudStorageAPI) DeleteBucketHandler(w http.ResponseWriter, req *http. errorIf(err.Trace(), "DeleteBucket failed.", nil) switch err.ToGoError().(type) { case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.BucketNotEmpty: - writeErrorResponse(w, req, BucketNotEmpty, req.URL.Path) + writeErrorResponse(w, r, BucketNotEmpty, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } diff --git a/generic-handlers.go b/generic-handlers.go index 998e11832..e2802a25d 100644 --- a/generic-handlers.go +++ b/generic-handlers.go @@ -26,6 +26,10 @@ import ( "github.com/rs/cors" ) +const ( + iso8601Format = "20060102T150405Z" +) + // HandlerFunc - useful to chain different middleware http.Handler type HandlerFunc func(http.Handler) http.Handler @@ -157,13 +161,12 @@ func setIgnoreSignatureV2RequestHandler(h http.Handler) http.Handler { // Ignore signature version '2' ServerHTTP() wrapper. func (h ignoreSignatureV2RequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if _, ok := r.Header["Authorization"]; ok { - if !strings.HasPrefix(r.Header.Get("Authorization"), authHeaderPrefix) { - writeErrorResponse(w, r, SignatureVersionNotSupported, r.URL.Path) - return - } + if isRequestSignatureV4(r) || isRequestJWT(r) || isRequestPresignedSignatureV4(r) || isRequestPostPolicySignatureV4(r) { + h.handler.ServeHTTP(w, r) + return } - h.handler.ServeHTTP(w, r) + writeErrorResponse(w, r, SignatureVersionNotSupported, r.URL.Path) + return } // setIgnoreResourcesHandler - diff --git a/jwt-auth-handler.go b/jwt-auth-handler.go deleted file mode 100644 index bc64676b8..000000000 --- a/jwt-auth-handler.go +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Minio Cloud Storage, (C) 2016 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 main - -import ( - "fmt" - "net/http" - - jwtgo "github.com/dgrijalva/jwt-go" -) - -type jwtAuthHandler struct { - handler http.Handler -} - -// setJWTAuthHandler - -// Verify if authorization header is of form JWT, reject it otherwise. -func setJWTAuthHandler(h http.Handler) http.Handler { - return jwtAuthHandler{h} -} - -// Ignore request if authorization header is not valid. -func (h jwtAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Let the top level caller handle if the requests should be - // allowed, if there are no Authorization headers. - if r.Header.Get("Authorization") == "" { - h.handler.ServeHTTP(w, r) - return - } - // Validate Authorization header to be valid. - jwt := InitJWT() - token, e := jwtgo.ParseFromRequest(r, func(token *jwtgo.Token) (interface{}, error) { - if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - return jwt.secretAccessKey, nil - }) - if e != nil || !token.Valid { - w.WriteHeader(http.StatusUnauthorized) - return - } - h.handler.ServeHTTP(w, r) -} diff --git a/logger_test.go b/logger_test.go deleted file mode 100644 index 09c654218..000000000 --- a/logger_test.go +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Minio Cloud 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 main - -import ( - "bytes" - "encoding/json" - "errors" - - "github.com/Sirupsen/logrus" - "github.com/minio/minio/pkg/probe" - - . "gopkg.in/check.v1" -) - -type LoggerSuite struct{} - -var _ = Suite(&LoggerSuite{}) - -func (s *LoggerSuite) TestLogger(c *C) { - var buffer bytes.Buffer - var fields logrus.Fields - log.Out = &buffer - log.Formatter = new(logrus.JSONFormatter) - - errorIf(probe.NewError(errors.New("Fake error")), "Failed with error.", nil) - err := json.Unmarshal(buffer.Bytes(), &fields) - c.Assert(err, IsNil) - c.Assert(fields["level"], Equals, "error") - - msg, ok := fields["Error"] - c.Assert(ok, Equals, true) - c.Assert(msg.(map[string]interface{})["cause"], Equals, "Fake error") -} diff --git a/object-handlers.go b/object-handlers.go index 12271b4ab..2117b7c54 100644 --- a/object-handlers.go +++ b/object-handlers.go @@ -23,8 +23,6 @@ import ( "github.com/gorilla/mux" "github.com/minio/minio/pkg/fs" - "github.com/minio/minio/pkg/probe" - v4 "github.com/minio/minio/pkg/signature" ) const ( @@ -53,48 +51,53 @@ func setResponseHeaders(w http.ResponseWriter, reqParams url.Values) { // ---------- // This implementation of the GET operation retrieves object. To use GET, // you must have READ access to the object. -func (api CloudStorageAPI) GetObjectHandler(w http.ResponseWriter, req *http.Request) { +func (api CloudStorageAPI) GetObjectHandler(w http.ResponseWriter, r *http.Request) { var object, bucket string - vars := mux.Vars(req) + vars := mux.Vars(r) bucket = vars["bucket"] object = vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + metadata, err := api.Filesystem.GetObjectMetadata(bucket, object) if err != nil { errorIf(err.Trace(), "GetObject failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } var hrange *httpRange - hrange, err = getRequestedRange(req.Header.Get("Range"), metadata.Size) + hrange, err = getRequestedRange(r.Header.Get("Range"), metadata.Size) if err != nil { - writeErrorResponse(w, req, InvalidRange, req.URL.Path) + writeErrorResponse(w, r, InvalidRange, r.URL.Path) return } // Set standard object headers. setObjectHeaders(w, metadata, hrange) - // Set any additional requested response headers. - setResponseHeaders(w, req.URL.Query()) + // Set any additional ruested response headers. + setResponseHeaders(w, r.URL.Query()) // Get the object. if _, err = api.Filesystem.GetObject(w, bucket, object, hrange.start, hrange.length); err != nil { @@ -106,32 +109,37 @@ func (api CloudStorageAPI) GetObjectHandler(w http.ResponseWriter, req *http.Req // HeadObjectHandler - HEAD Object // ----------- // The HEAD operation retrieves metadata from an object without returning the object itself. -func (api CloudStorageAPI) HeadObjectHandler(w http.ResponseWriter, req *http.Request) { +func (api CloudStorageAPI) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { var object, bucket string - vars := mux.Vars(req) + vars := mux.Vars(r) bucket = vars["bucket"] object = vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + metadata, err := api.Filesystem.GetObjectMetadata(bucket, object) if err != nil { switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -142,86 +150,63 @@ func (api CloudStorageAPI) HeadObjectHandler(w http.ResponseWriter, req *http.Re // PutObjectHandler - PUT Object // ---------- // This implementation of the PUT operation adds an object to a bucket. -func (api CloudStorageAPI) PutObjectHandler(w http.ResponseWriter, req *http.Request) { +func (api CloudStorageAPI) PutObjectHandler(w http.ResponseWriter, r *http.Request) { var object, bucket string - vars := mux.Vars(req) + vars := mux.Vars(r) bucket = vars["bucket"] object = vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } // get Content-MD5 sent by client and verify if valid - md5 := req.Header.Get("Content-MD5") + md5 := r.Header.Get("Content-MD5") if !isValidMD5(md5) { - writeErrorResponse(w, req, InvalidDigest, req.URL.Path) + writeErrorResponse(w, r, InvalidDigest, r.URL.Path) return } /// if Content-Length is unknown/missing, deny the request - size := req.ContentLength - if size == -1 { - writeErrorResponse(w, req, MissingContentLength, req.URL.Path) + size := r.ContentLength + if size == -1 && !contains(r.TransferEncoding, "chunked") { + writeErrorResponse(w, r, MissingContentLength, r.URL.Path) return } /// maximum Upload size for objects in a single operation if isMaxObjectSize(size) { - writeErrorResponse(w, req, EntityTooLarge, req.URL.Path) + writeErrorResponse(w, r, EntityTooLarge, r.URL.Path) return } - var signature *v4.Signature - if isRequestSignatureV4(req) { - // Init signature V4 verification - var err *probe.Error - signature, err = initSignatureV4(req) - if err != nil { - switch err.ToGoError() { - case errInvalidRegion: - errorIf(err.Trace(), "Unknown region in authorization header.", nil) - writeErrorResponse(w, req, AuthorizationHeaderMalformed, req.URL.Path) - return - case errAccessKeyIDInvalid: - errorIf(err.Trace(), "Invalid access key id.", nil) - writeErrorResponse(w, req, InvalidAccessKeyID, req.URL.Path) - return - default: - errorIf(err.Trace(), "Initializing signature v4 failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) - return - } - } - } + // Set http request for signature. + api.Signature.SetHTTPRequestToVerify(r) - metadata, err := api.Filesystem.CreateObject(bucket, object, md5, size, req.Body, signature) + // Create object. + metadata, err := api.Filesystem.CreateObject(bucket, object, md5, size, r.Body, api.Signature) if err != nil { errorIf(err.Trace(), "CreateObject failed.", nil) switch err.ToGoError().(type) { case fs.RootPathFull: - writeErrorResponse(w, req, RootPathFull, req.URL.Path) + writeErrorResponse(w, r, RootPathFull, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BadDigest: - writeErrorResponse(w, req, BadDigest, req.URL.Path) - case fs.MissingDateHeader: - writeErrorResponse(w, req, RequestTimeTooSkewed, req.URL.Path) - case v4.SigDoesNotMatch: - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) + writeErrorResponse(w, r, BadDigest, r.URL.Path) + case fs.SignDoesNotMatch: + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) case fs.IncompleteBody: - writeErrorResponse(w, req, IncompleteBody, req.URL.Path) - case fs.EntityTooLarge: - writeErrorResponse(w, req, EntityTooLarge, req.URL.Path) + writeErrorResponse(w, r, IncompleteBody, r.URL.Path) case fs.InvalidDigest: - writeErrorResponse(w, req, InvalidDigest, req.URL.Path) + writeErrorResponse(w, r, InvalidDigest, r.URL.Path) case fs.ObjectExistsAsPrefix: - writeErrorResponse(w, req, ObjectExistsAsPrefix, req.URL.Path) + writeErrorResponse(w, r, ObjectExistsAsPrefix, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -234,35 +219,40 @@ func (api CloudStorageAPI) PutObjectHandler(w http.ResponseWriter, req *http.Req /// Multipart CloudStorageAPI // NewMultipartUploadHandler - New multipart upload -func (api CloudStorageAPI) NewMultipartUploadHandler(w http.ResponseWriter, req *http.Request) { +func (api CloudStorageAPI) NewMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { var object, bucket string - vars := mux.Vars(req) + vars := mux.Vars(r) bucket = vars["bucket"] object = vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + uploadID, err := api.Filesystem.NewMultipartUpload(bucket, object) if err != nil { errorIf(err.Trace(), "NewMultipartUpload failed.", nil) switch err.ToGoError().(type) { case fs.RootPathFull: - writeErrorResponse(w, req, RootPathFull, req.URL.Path) + writeErrorResponse(w, r, RootPathFull, r.URL.Path) case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -276,94 +266,72 @@ func (api CloudStorageAPI) NewMultipartUploadHandler(w http.ResponseWriter, req } // PutObjectPartHandler - Upload part -func (api CloudStorageAPI) PutObjectPartHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) PutObjectPartHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] object := vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } // get Content-MD5 sent by client and verify if valid - md5 := req.Header.Get("Content-MD5") + md5 := r.Header.Get("Content-MD5") if !isValidMD5(md5) { - writeErrorResponse(w, req, InvalidDigest, req.URL.Path) + writeErrorResponse(w, r, InvalidDigest, r.URL.Path) return } /// if Content-Length is unknown/missing, throw away - size := req.ContentLength + size := r.ContentLength if size == -1 { - writeErrorResponse(w, req, MissingContentLength, req.URL.Path) + writeErrorResponse(w, r, MissingContentLength, r.URL.Path) return } /// maximum Upload size for multipart objects in a single operation if isMaxObjectSize(size) { - writeErrorResponse(w, req, EntityTooLarge, req.URL.Path) + writeErrorResponse(w, r, EntityTooLarge, r.URL.Path) return } - uploadID := req.URL.Query().Get("uploadId") - partIDString := req.URL.Query().Get("partNumber") + uploadID := r.URL.Query().Get("uploadId") + partIDString := r.URL.Query().Get("partNumber") var partID int { var err error partID, err = strconv.Atoi(partIDString) if err != nil { - writeErrorResponse(w, req, InvalidPart, req.URL.Path) + writeErrorResponse(w, r, InvalidPart, r.URL.Path) return } } - var signature *v4.Signature - if isRequestSignatureV4(req) { - // Init signature V4 verification - var err *probe.Error - signature, err = initSignatureV4(req) - if err != nil { - switch err.ToGoError() { - case errInvalidRegion: - errorIf(err.Trace(), "Unknown region in authorization header.", nil) - writeErrorResponse(w, req, AuthorizationHeaderMalformed, req.URL.Path) - return - case errAccessKeyIDInvalid: - errorIf(err.Trace(), "Invalid access key id.", nil) - writeErrorResponse(w, req, InvalidAccessKeyID, req.URL.Path) - return - default: - errorIf(err.Trace(), "Initializing signature v4 failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) - return - } - } - } + // Set http request. + api.Signature.SetHTTPRequestToVerify(r) - calculatedMD5, err := api.Filesystem.CreateObjectPart(bucket, object, uploadID, md5, partID, size, req.Body, signature) + calculatedMD5, err := api.Filesystem.CreateObjectPart(bucket, object, uploadID, md5, partID, size, r.Body, api.Signature) if err != nil { errorIf(err.Trace(), "CreateObjectPart failed.", nil) switch err.ToGoError().(type) { case fs.RootPathFull: - writeErrorResponse(w, req, RootPathFull, req.URL.Path) + writeErrorResponse(w, r, RootPathFull, r.URL.Path) case fs.InvalidUploadID: - writeErrorResponse(w, req, NoSuchUpload, req.URL.Path) + writeErrorResponse(w, r, NoSuchUpload, r.URL.Path) case fs.BadDigest: - writeErrorResponse(w, req, BadDigest, req.URL.Path) - case v4.SigDoesNotMatch: - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) + writeErrorResponse(w, r, BadDigest, r.URL.Path) + case fs.SignDoesNotMatch: + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) case fs.IncompleteBody: - writeErrorResponse(w, req, IncompleteBody, req.URL.Path) - case fs.EntityTooLarge: - writeErrorResponse(w, req, EntityTooLarge, req.URL.Path) + writeErrorResponse(w, r, IncompleteBody, r.URL.Path) case fs.InvalidDigest: - writeErrorResponse(w, req, InvalidDigest, req.URL.Path) + writeErrorResponse(w, r, InvalidDigest, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -374,35 +342,40 @@ func (api CloudStorageAPI) PutObjectPartHandler(w http.ResponseWriter, req *http } // AbortMultipartUploadHandler - Abort multipart upload -func (api CloudStorageAPI) AbortMultipartUploadHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] object := vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } - objectResourcesMetadata := getObjectResources(req.URL.Query()) + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + + objectResourcesMetadata := getObjectResources(r.URL.Query()) err := api.Filesystem.AbortMultipartUpload(bucket, object, objectResourcesMetadata.UploadID) if err != nil { errorIf(err.Trace(), "AbortMutlipartUpload failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.InvalidUploadID: - writeErrorResponse(w, req, NoSuchUpload, req.URL.Path) + writeErrorResponse(w, r, NoSuchUpload, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -410,25 +383,30 @@ func (api CloudStorageAPI) AbortMultipartUploadHandler(w http.ResponseWriter, re } // ListObjectPartsHandler - List object parts -func (api CloudStorageAPI) ListObjectPartsHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) ListObjectPartsHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] object := vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } - objectResourcesMetadata := getObjectResources(req.URL.Query()) + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + + objectResourcesMetadata := getObjectResources(r.URL.Query()) if objectResourcesMetadata.PartNumberMarker < 0 { - writeErrorResponse(w, req, InvalidPartNumberMarker, req.URL.Path) + writeErrorResponse(w, r, InvalidPartNumberMarker, r.URL.Path) return } if objectResourcesMetadata.MaxParts < 0 { - writeErrorResponse(w, req, InvalidMaxParts, req.URL.Path) + writeErrorResponse(w, r, InvalidMaxParts, r.URL.Path) return } if objectResourcesMetadata.MaxParts == 0 { @@ -440,17 +418,17 @@ func (api CloudStorageAPI) ListObjectPartsHandler(w http.ResponseWriter, req *ht errorIf(err.Trace(), "ListObjectParts failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.InvalidUploadID: - writeErrorResponse(w, req, NoSuchUpload, req.URL.Path) + writeErrorResponse(w, r, NoSuchUpload, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } @@ -463,72 +441,55 @@ func (api CloudStorageAPI) ListObjectPartsHandler(w http.ResponseWriter, req *ht } // CompleteMultipartUploadHandler - Complete multipart upload -func (api CloudStorageAPI) CompleteMultipartUploadHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] object := vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } - objectResourcesMetadata := getObjectResources(req.URL.Query()) - var signature *v4.Signature - if isRequestSignatureV4(req) { - // Init signature V4 verification - var err *probe.Error - signature, err = initSignatureV4(req) - if err != nil { - switch err.ToGoError() { - case errInvalidRegion: - errorIf(err.Trace(), "Unknown region in authorization header.", nil) - writeErrorResponse(w, req, AuthorizationHeaderMalformed, req.URL.Path) - return - case errAccessKeyIDInvalid: - errorIf(err.Trace(), "Invalid access key id.", nil) - writeErrorResponse(w, req, InvalidAccessKeyID, req.URL.Path) - return - default: - errorIf(err.Trace(), "Initializing signature v4 failed.", nil) - writeErrorResponse(w, req, InternalError, req.URL.Path) - return - } - } - } + // Set http request for signature. + api.Signature.SetHTTPRequestToVerify(r) + + // Extract object resources. + objectResourcesMetadata := getObjectResources(r.URL.Query()) - metadata, err := api.Filesystem.CompleteMultipartUpload(bucket, object, objectResourcesMetadata.UploadID, req.Body, signature) + // Complete multipart upload. + metadata, err := api.Filesystem.CompleteMultipartUpload(bucket, object, objectResourcesMetadata.UploadID, r.Body, api.Signature) if err != nil { errorIf(err.Trace(), "CompleteMultipartUpload failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.InvalidUploadID: - writeErrorResponse(w, req, NoSuchUpload, req.URL.Path) + writeErrorResponse(w, r, NoSuchUpload, r.URL.Path) case fs.InvalidPart: - writeErrorResponse(w, req, InvalidPart, req.URL.Path) + writeErrorResponse(w, r, InvalidPart, r.URL.Path) case fs.InvalidPartOrder: - writeErrorResponse(w, req, InvalidPartOrder, req.URL.Path) - case v4.SigDoesNotMatch: - writeErrorResponse(w, req, SignatureDoesNotMatch, req.URL.Path) + writeErrorResponse(w, r, InvalidPartOrder, r.URL.Path) + case fs.SignDoesNotMatch: + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) case fs.IncompleteBody: - writeErrorResponse(w, req, IncompleteBody, req.URL.Path) + writeErrorResponse(w, r, IncompleteBody, r.URL.Path) case fs.MalformedXML: - writeErrorResponse(w, req, MalformedXML, req.URL.Path) + writeErrorResponse(w, r, MalformedXML, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } return } - response := generateCompleteMultpartUploadResponse(bucket, object, req.URL.String(), metadata.MD5) + response := generateCompleteMultpartUploadResponse(bucket, object, r.URL.String(), metadata.MD5) encodedSuccessResponse := encodeSuccessResponse(response) // write headers setCommonHeaders(w) @@ -539,32 +500,37 @@ func (api CloudStorageAPI) CompleteMultipartUploadHandler(w http.ResponseWriter, /// Delete CloudStorageAPI // DeleteObjectHandler - Delete object -func (api CloudStorageAPI) DeleteObjectHandler(w http.ResponseWriter, req *http.Request) { - vars := mux.Vars(req) +func (api CloudStorageAPI) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) bucket := vars["bucket"] object := vars["object"] - if isRequestRequiresACLCheck(req) { + if isRequestRequiresACLCheck(r) { if api.Filesystem.IsPrivateBucket(bucket) || api.Filesystem.IsReadOnlyBucket(bucket) { - writeErrorResponse(w, req, AccessDenied, req.URL.Path) + writeErrorResponse(w, r, AccessDenied, r.URL.Path) return } } + if !isSignV4ReqAuthenticated(api.Signature, r) { + writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) + return + } + err := api.Filesystem.DeleteObject(bucket, object) if err != nil { errorIf(err.Trace(), "DeleteObject failed.", nil) switch err.ToGoError().(type) { case fs.BucketNameInvalid: - writeErrorResponse(w, req, InvalidBucketName, req.URL.Path) + writeErrorResponse(w, r, InvalidBucketName, r.URL.Path) case fs.BucketNotFound: - writeErrorResponse(w, req, NoSuchBucket, req.URL.Path) + writeErrorResponse(w, r, NoSuchBucket, r.URL.Path) case fs.ObjectNotFound: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) case fs.ObjectNameInvalid: - writeErrorResponse(w, req, NoSuchKey, req.URL.Path) + writeErrorResponse(w, r, NoSuchKey, r.URL.Path) default: - writeErrorResponse(w, req, InternalError, req.URL.Path) + writeErrorResponse(w, r, InternalError, r.URL.Path) } } writeSuccessNoContent(w) diff --git a/pkg/atomic/atomic.go b/pkg/atomic/atomic.go index 2912a8cf1..4e5fe10df 100644 --- a/pkg/atomic/atomic.go +++ b/pkg/atomic/atomic.go @@ -23,7 +23,6 @@ import ( "io/ioutil" "os" "path/filepath" - "strings" ) // File container provided for atomic file writes @@ -82,7 +81,6 @@ func FileCreateWithPrefix(filePath string, prefix string) (*File, error) { if err := os.MkdirAll(filepath.Dir(filePath), 0700); err != nil { return nil, err } - prefix = strings.TrimSpace(prefix) f, err := ioutil.TempFile(filepath.Dir(filePath), prefix+filepath.Base(filePath)) if err != nil { return nil, err diff --git a/pkg/fs/definitions.go b/pkg/fs/definitions.go index a69f6a7f4..69fd681b7 100644 --- a/pkg/fs/definitions.go +++ b/pkg/fs/definitions.go @@ -19,7 +19,6 @@ package fs import ( "os" "regexp" - "strings" "time" "unicode/utf8" ) @@ -167,7 +166,7 @@ var validBucket = regexp.MustCompile(`^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$`) // IsValidBucketName - verify bucket name in accordance with // - http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html func IsValidBucketName(bucket string) bool { - if strings.TrimSpace(bucket) == "" { + if bucket == "" { return false } if len(bucket) < 3 || len(bucket) > 63 { @@ -182,7 +181,7 @@ func IsValidBucketName(bucket string) bool { // IsValidObjectName - verify object name in accordance with // - http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html func IsValidObjectName(object string) bool { - if strings.TrimSpace(object) == "" { + if object == "" { return true } if len(object) > 1024 || len(object) == 0 { diff --git a/pkg/fs/errors.go b/pkg/fs/errors.go index 550629480..8ac82b9aa 100644 --- a/pkg/fs/errors.go +++ b/pkg/fs/errors.go @@ -18,25 +18,11 @@ package fs import "fmt" -// MissingDateHeader date header missing -type MissingDateHeader struct{} +// SignDoesNotMatch - signature does not match. +type SignDoesNotMatch struct{} -func (e MissingDateHeader) Error() string { - return "Missing date header" -} - -// MissingExpiresQuery expires query string missing -type MissingExpiresQuery struct{} - -func (e MissingExpiresQuery) Error() string { - return "Missing expires query string" -} - -// ExpiredPresignedRequest request already expired -type ExpiredPresignedRequest struct{} - -func (e ExpiredPresignedRequest) Error() string { - return "Presigned request already expired" +func (e SignDoesNotMatch) Error() string { + return "Signature does not match." } // InvalidArgument invalid argument @@ -156,30 +142,8 @@ func (e BadDigest) Error() string { return "Bad digest" } -// ParityOverflow parity over flow -type ParityOverflow struct{} - -func (e ParityOverflow) Error() string { - return "Parity overflow" -} - -// ChecksumMismatch checksum mismatch -type ChecksumMismatch struct{} - -func (e ChecksumMismatch) Error() string { - return "Checksum mismatch" -} - -// MissingPOSTPolicy missing post policy -type MissingPOSTPolicy struct{} - -func (e MissingPOSTPolicy) Error() string { - return "Missing POST policy in multipart form" -} - // InternalError - generic internal error -type InternalError struct { -} +type InternalError struct{} // BackendError - generic disk backend error type BackendError struct { @@ -237,13 +201,6 @@ type BucketNameInvalid GenericBucketError /// Object related errors -// EntityTooLarge - object size exceeds maximum limit -type EntityTooLarge struct { - GenericObjectError - Size string - MaxSize string -} - // ObjectNameInvalid - object name provided is invalid type ObjectNameInvalid GenericObjectError @@ -292,11 +249,6 @@ func (e ObjectNameInvalid) Error() string { return "Object name invalid: " + e.Bucket + "#" + e.Object } -// Return string an error formatted as the given text -func (e EntityTooLarge) Error() string { - return e.Bucket + "#" + e.Object + "with " + e.Size + "reached maximum allowed size limit " + e.MaxSize -} - // IncompleteBody You did not provide the number of bytes specified by the Content-Length HTTP header type IncompleteBody GenericObjectError diff --git a/pkg/fs/fs-bucket-listobjects.go b/pkg/fs/fs-bucket-listobjects.go index 2004ed92d..73561bcde 100644 --- a/pkg/fs/fs-bucket-listobjects.go +++ b/pkg/fs/fs-bucket-listobjects.go @@ -68,9 +68,11 @@ func (fs Filesystem) listObjects(bucket, prefix, marker, delimiter string, maxKe // Bucket path prefix should always end with a separator. bucketPathPrefix := bucketPath + string(os.PathSeparator) prefixPath := bucketPathPrefix + prefix - st, err := os.Stat(prefixPath) - if err != nil && os.IsNotExist(err) { - walkPath = bucketPath + st, e := os.Stat(prefixPath) + if e != nil { + if os.IsNotExist(e) { + walkPath = bucketPath + } } else { if st.IsDir() && !strings.HasSuffix(prefix, delimiter) { walkPath = bucketPath diff --git a/pkg/fs/fs-bucket.go b/pkg/fs/fs-bucket.go index 0a6ac14ee..45720d738 100644 --- a/pkg/fs/fs-bucket.go +++ b/pkg/fs/fs-bucket.go @@ -152,7 +152,7 @@ func (fs Filesystem) MakeBucket(bucket, acl string) *probe.Error { } return probe.NewError(e) } - if strings.TrimSpace(acl) == "" { + if acl == "" { acl = "private" } @@ -232,7 +232,7 @@ func (fs Filesystem) SetBucketMetadata(bucket string, metadata map[string]string if !IsValidBucketACL(acl) { return probe.NewError(InvalidACL{ACL: acl}) } - if strings.TrimSpace(acl) == "" { + if acl == "" { acl = "private" } bucket = fs.denormalizeBucket(bucket) diff --git a/pkg/fs/fs-multipart.go b/pkg/fs/fs-multipart.go index 672d9e57a..a7f51631b 100644 --- a/pkg/fs/fs-multipart.go +++ b/pkg/fs/fs-multipart.go @@ -174,7 +174,15 @@ func saveParts(partPathPrefix string, mw io.Writer, parts []CompletePart) *probe md5Sum = strings.TrimSuffix(md5Sum, "\"") partFile, e := os.OpenFile(partPathPrefix+md5Sum+fmt.Sprintf("$%d-$multiparts", part.PartNumber), os.O_RDONLY, 0600) if e != nil { - return probe.NewError(e) + if !os.IsNotExist(e) { + return probe.NewError(e) + } + // Some clients do not set Content-MD5, so we would have + // created part files without 'ETag' in them. + partFile, e = os.OpenFile(partPathPrefix+fmt.Sprintf("$%d-$multiparts", part.PartNumber), os.O_RDONLY, 0600) + if e != nil { + return probe.NewError(e) + } } partReaders = append(partReaders, partFile) partClosers = append(partClosers, partFile) @@ -322,9 +330,9 @@ func (fs Filesystem) CreateObjectPart(bucket, object, uploadID, expectedMD5Sum s return "", probe.NewError(InvalidUploadID{UploadID: uploadID}) } - if strings.TrimSpace(expectedMD5Sum) != "" { + if expectedMD5Sum != "" { var expectedMD5SumBytes []byte - expectedMD5SumBytes, err = base64.StdEncoding.DecodeString(strings.TrimSpace(expectedMD5Sum)) + expectedMD5SumBytes, err = base64.StdEncoding.DecodeString(expectedMD5Sum) if err != nil { // Pro-actively close the connection return "", probe.NewError(InvalidDigest{MD5: expectedMD5Sum}) @@ -361,8 +369,8 @@ func (fs Filesystem) CreateObjectPart(bucket, object, uploadID, expectedMD5Sum s md5sum := hex.EncodeToString(md5Hasher.Sum(nil)) // Verify if the written object is equal to what is expected, only // if it is requested as such. - if strings.TrimSpace(expectedMD5Sum) != "" { - if !isMD5SumEqual(strings.TrimSpace(expectedMD5Sum), md5sum) { + if expectedMD5Sum != "" { + if !isMD5SumEqual(expectedMD5Sum, md5sum) { partFile.CloseAndPurge() return "", probe.NewError(BadDigest{MD5: expectedMD5Sum, Bucket: bucket, Object: object}) } @@ -375,7 +383,7 @@ func (fs Filesystem) CreateObjectPart(bucket, object, uploadID, expectedMD5Sum s } if !ok { partFile.CloseAndPurge() - return "", probe.NewError(signV4.SigDoesNotMatch{}) + return "", probe.NewError(SignDoesNotMatch{}) } } partFile.Close() @@ -472,7 +480,7 @@ func (fs Filesystem) CompleteMultipartUpload(bucket, object, uploadID string, da } if !ok { file.CloseAndPurge() - return ObjectMetadata{}, probe.NewError(signV4.SigDoesNotMatch{}) + return ObjectMetadata{}, probe.NewError(SignDoesNotMatch{}) } } completeMultipartUpload := &CompleteMultipartUpload{} diff --git a/pkg/fs/fs-object.go b/pkg/fs/fs-object.go index e9e000952..bb9cc123b 100644 --- a/pkg/fs/fs-object.go +++ b/pkg/fs/fs-object.go @@ -178,7 +178,7 @@ func getMetadata(rootPath, bucket, object string) (ObjectMetadata, *probe.Error) // isMD5SumEqual - returns error if md5sum mismatches, success its `nil` func isMD5SumEqual(expectedMD5Sum, actualMD5Sum string) bool { // Verify the md5sum. - if strings.TrimSpace(expectedMD5Sum) != "" && strings.TrimSpace(actualMD5Sum) != "" { + if expectedMD5Sum != "" && actualMD5Sum != "" { // Decode md5sum to bytes from their hexadecimal // representations. expectedMD5SumBytes, err := hex.DecodeString(expectedMD5Sum) @@ -199,7 +199,7 @@ func isMD5SumEqual(expectedMD5Sum, actualMD5Sum string) bool { } // CreateObject - create an object. -func (fs Filesystem) CreateObject(bucket, object, expectedMD5Sum string, size int64, data io.Reader, signature *signV4.Signature) (ObjectMetadata, *probe.Error) { +func (fs Filesystem) CreateObject(bucket, object, expectedMD5Sum string, size int64, data io.Reader, sig *signV4.Signature) (ObjectMetadata, *probe.Error) { di, e := disk.GetInfo(fs.path) if e != nil { return ObjectMetadata{}, probe.NewError(e) @@ -233,9 +233,9 @@ func (fs Filesystem) CreateObject(bucket, object, expectedMD5Sum string, size in // Get object path. objectPath := filepath.Join(bucketPath, object) - if strings.TrimSpace(expectedMD5Sum) != "" { + if expectedMD5Sum != "" { var expectedMD5SumBytes []byte - expectedMD5SumBytes, e = base64.StdEncoding.DecodeString(strings.TrimSpace(expectedMD5Sum)) + expectedMD5SumBytes, e = base64.StdEncoding.DecodeString(expectedMD5Sum) if e != nil { // Pro-actively close the connection. return ObjectMetadata{}, probe.NewError(InvalidDigest{MD5: expectedMD5Sum}) @@ -244,7 +244,7 @@ func (fs Filesystem) CreateObject(bucket, object, expectedMD5Sum string, size in } // Write object. - file, e := atomic.FileCreateWithPrefix(objectPath, "$tmpobject") + file, e := atomic.FileCreateWithPrefix(objectPath, expectedMD5Sum+"$tmpobject") if e != nil { switch e := e.(type) { case *os.PathError: @@ -279,22 +279,22 @@ func (fs Filesystem) CreateObject(bucket, object, expectedMD5Sum string, size in md5Sum := hex.EncodeToString(md5Hasher.Sum(nil)) // Verify if the written object is equal to what is expected, only // if it is requested as such. - if strings.TrimSpace(expectedMD5Sum) != "" { - if !isMD5SumEqual(strings.TrimSpace(expectedMD5Sum), md5Sum) { + if expectedMD5Sum != "" { + if !isMD5SumEqual(expectedMD5Sum, md5Sum) { file.CloseAndPurge() return ObjectMetadata{}, probe.NewError(BadDigest{MD5: expectedMD5Sum, Bucket: bucket, Object: object}) } } sha256Sum := hex.EncodeToString(sha256Hasher.Sum(nil)) - if signature != nil { - ok, err := signature.DoesSignatureMatch(sha256Sum) + if sig != nil { + ok, err := sig.DoesSignatureMatch(sha256Sum) if err != nil { file.CloseAndPurge() return ObjectMetadata{}, err.Trace() } if !ok { file.CloseAndPurge() - return ObjectMetadata{}, probe.NewError(signV4.SigDoesNotMatch{}) + return ObjectMetadata{}, signV4.ErrSignDoesNotMath("Signature does not match") } } file.Close() diff --git a/pkg/signature/errors.go b/pkg/signature/errors.go index 961a42390..58aaea2e9 100644 --- a/pkg/signature/errors.go +++ b/pkg/signature/errors.go @@ -16,33 +16,41 @@ package signature -// MissingDateHeader date header missing -type MissingDateHeader struct{} +import ( + "fmt" -func (e MissingDateHeader) Error() string { - return "Missing date header" -} - -// MissingExpiresQuery expires query string missing -type MissingExpiresQuery struct{} - -func (e MissingExpiresQuery) Error() string { - return "Missing expires query string" -} + "github.com/minio/minio/pkg/probe" +) -// ExpiredPresignedRequest request already expired -type ExpiredPresignedRequest struct{} +type errFunc func(msg string, a ...string) *probe.Error -func (e ExpiredPresignedRequest) Error() string { - return "Presigned request already expired" +func errFactory() errFunc { + return func(msg string, a ...string) *probe.Error { + return probe.NewError(fmt.Errorf("%s, Args: %s", msg, a)).Untrace() + } } -// SigDoesNotMatch invalid signature -type SigDoesNotMatch struct { - SignatureSent string - SignatureCalculated string -} - -func (e SigDoesNotMatch) Error() string { - return "The request signature we calculated does not match the signature you provided" -} +// Various errors. +var ( + ErrPolicyAlreadyExpired = errFactory() + ErrInvalidRegion = errFactory() + ErrInvalidDateFormat = errFactory() + ErrInvalidService = errFactory() + ErrInvalidRequestVersion = errFactory() + ErrMissingFields = errFactory() + ErrMissingCredTag = errFactory() + ErrCredMalformed = errFactory() + ErrMissingSignTag = errFactory() + ErrMissingSignHeadersTag = errFactory() + ErrMissingDateHeader = errFactory() + ErrMalformedDate = errFactory() + ErrMalformedExpires = errFactory() + ErrAuthHeaderEmpty = errFactory() + ErrUnsuppSignAlgo = errFactory() + ErrMissingExpiresQuery = errFactory() + ErrExpiredPresignRequest = errFactory() + ErrSignDoesNotMath = errFactory() + ErrInvalidAccessKeyID = errFactory() + ErrInvalidSecretKey = errFactory() + ErrRegionISEmpty = errFactory() +) diff --git a/pkg/signature/postpolicyform.go b/pkg/signature/postpolicyform.go index 982cc5048..006c6d848 100644 --- a/pkg/signature/postpolicyform.go +++ b/pkg/signature/postpolicyform.go @@ -17,9 +17,11 @@ package signature import ( + "encoding/base64" "encoding/json" "fmt" "reflect" + "strings" "time" "github.com/minio/minio/pkg/probe" @@ -67,8 +69,8 @@ type PostPolicyForm struct { } } -// ParsePostPolicyForm - Parse JSON policy string into typed POostPolicyForm structure. -func ParsePostPolicyForm(policy string) (PostPolicyForm, *probe.Error) { +// parsePostPolicyFormV4 - Parse JSON policy string into typed POostPolicyForm structure. +func parsePostPolicyFormV4(policy string) (PostPolicyForm, *probe.Error) { // Convert po into interfaces and // perform strict type conversion using reflection. var rawPolicy struct { @@ -155,3 +157,53 @@ func ParsePostPolicyForm(policy string) (PostPolicyForm, *probe.Error) { } return parsedPolicy, nil } + +// ApplyPolicyCond - apply policy conditions and validate input values. +func ApplyPolicyCond(formValues map[string]string) *probe.Error { + if formValues["X-Amz-Algorithm"] != signV4Algorithm { + return ErrUnsuppSignAlgo("Unsupported signature algorithm in policy form data.", formValues["X-Amz-Algorithm"]).Trace(formValues["X-Amz-Algorithm"]) + } + /// Decoding policy + policyBytes, e := base64.StdEncoding.DecodeString(formValues["Policy"]) + if e != nil { + return probe.NewError(e) + } + postPolicyForm, err := parsePostPolicyFormV4(string(policyBytes)) + if err != nil { + return err.Trace() + } + if !postPolicyForm.Expiration.After(time.Now().UTC()) { + return ErrPolicyAlreadyExpired("Policy has already expired, please generate a new one.") + } + if postPolicyForm.Conditions.Policies["$bucket"].Operator == "eq" { + if formValues["Bucket"] != postPolicyForm.Conditions.Policies["$bucket"].Value { + return ErrMissingFields("Policy bucket is missing.", formValues["Bucket"]) + } + } + if postPolicyForm.Conditions.Policies["$x-amz-date"].Operator == "eq" { + if formValues["X-Amz-Date"] != postPolicyForm.Conditions.Policies["$x-amz-date"].Value { + return ErrMissingFields("Policy date is missing.", formValues["X-Amz-Date"]) + } + } + if postPolicyForm.Conditions.Policies["$Content-Type"].Operator == "starts-with" { + if !strings.HasPrefix(formValues["Content-Type"], postPolicyForm.Conditions.Policies["$Content-Type"].Value) { + return ErrMissingFields("Policy content-type is missing or invalid.", formValues["Content-Type"]) + } + } + if postPolicyForm.Conditions.Policies["$Content-Type"].Operator == "eq" { + if formValues["Content-Type"] != postPolicyForm.Conditions.Policies["$Content-Type"].Value { + return ErrMissingFields("Policy content-Type is missing or invalid.", formValues["Content-Type"]) + } + } + if postPolicyForm.Conditions.Policies["$key"].Operator == "starts-with" { + if !strings.HasPrefix(formValues["Key"], postPolicyForm.Conditions.Policies["$key"].Value) { + return ErrMissingFields("Policy key is missing.", formValues["Key"]) + } + } + if postPolicyForm.Conditions.Policies["$key"].Operator == "eq" { + if formValues["Key"] != postPolicyForm.Conditions.Policies["$key"].Value { + return ErrMissingFields("Policy key is missing.", formValues["Key"]) + } + } + return nil +} diff --git a/pkg/signature/signature-v4.go b/pkg/signature/signature-v4.go index 5d8a10a64..d4e396b20 100644 --- a/pkg/signature/signature-v4.go +++ b/pkg/signature/signature-v4.go @@ -18,16 +18,13 @@ package signature import ( "bytes" - "crypto/hmac" "encoding/hex" "net/http" "net/url" - "regexp" "sort" "strconv" "strings" "time" - "unicode/utf8" "github.com/minio/minio/pkg/crypto/sha256" "github.com/minio/minio/pkg/probe" @@ -35,72 +32,52 @@ import ( // Signature - local variables type Signature struct { - AccessKeyID string - SecretAccessKey string - Region string - Presigned bool - PresignedPolicy string - SignedHeaders []string - Signature string - Request *http.Request + accessKeyID string + secretAccessKey string + region string + httpRequest *http.Request + extractedSignedHeaders http.Header } const ( - authHeaderPrefix = "AWS4-HMAC-SHA256" - iso8601Format = "20060102T150405Z" - yyyymmdd = "20060102" + signV4Algorithm = "AWS4-HMAC-SHA256" + iso8601Format = "20060102T150405Z" + yyyymmdd = "20060102" ) -// sumHMAC calculate hmac between two input byte array -func sumHMAC(key []byte, data []byte) []byte { - hash := hmac.New(sha256.New, key) - hash.Write(data) - return hash.Sum(nil) +// New - initialize a new authorization checkes. +func New(accessKeyID, secretAccessKey, region string) (*Signature, *probe.Error) { + if !isValidAccessKey.MatchString(accessKeyID) { + return nil, ErrInvalidAccessKeyID("Invalid access key id.", accessKeyID).Trace(accessKeyID) + } + if !isValidSecretKey.MatchString(secretAccessKey) { + return nil, ErrInvalidAccessKeyID("Invalid secret key.", secretAccessKey).Trace(secretAccessKey) + } + if region == "" { + return nil, ErrRegionISEmpty("Region is empty.").Trace() + } + signature := &Signature{ + accessKeyID: accessKeyID, + secretAccessKey: secretAccessKey, + region: region, + } + return signature, nil } -// getURLEncodedName encode the strings from UTF-8 byte representations to HTML hex escape sequences -// -// This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 -// non english characters cannot be parsed due to the nature in which url.Encode() is written -// -// This function on the other hand is a direct replacement for url.Encode() technique to support -// pretty much every UTF-8 character. -func getURLEncodedName(name string) string { - // if object matches reserved string, no need to encode them - reservedNames := regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") - if reservedNames.MatchString(name) { - return name - } - var encodedName string - for _, s := range name { - if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) - encodedName = encodedName + string(s) - continue - } - switch s { - case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) - encodedName = encodedName + string(s) - continue - default: - len := utf8.RuneLen(s) - if len < 0 { - return name - } - u := make([]byte, len) - utf8.EncodeRune(u, s) - for _, r := range u { - hex := hex.EncodeToString([]byte{r}) - encodedName = encodedName + "%" + strings.ToUpper(hex) - } - } +// SetHTTPRequestToVerify - sets the http request which needs to be verified. +func (s *Signature) SetHTTPRequestToVerify(r *http.Request) *Signature { + // Do not set http request if its 'nil'. + if r == nil { + return s } - return encodedName + s.httpRequest = r + return s } // getCanonicalHeaders generate a list of request headers with their values -func (r Signature) getCanonicalHeaders(signedHeaders map[string][]string) string { +func (s Signature) getCanonicalHeaders(signedHeaders http.Header) string { var headers []string - vals := make(map[string][]string) + vals := make(http.Header) for k, vv := range signedHeaders { headers = append(headers, strings.ToLower(k)) vals[strings.ToLower(k)] = vv @@ -114,7 +91,7 @@ func (r Signature) getCanonicalHeaders(signedHeaders map[string][]string) string buf.WriteByte(':') switch { case k == "host": - buf.WriteString(r.Request.Host) + buf.WriteString(s.httpRequest.Host) fallthrough default: for idx, v := range vals[k] { @@ -130,7 +107,7 @@ func (r Signature) getCanonicalHeaders(signedHeaders map[string][]string) string } // getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names -func (r Signature) getSignedHeaders(signedHeaders map[string][]string) string { +func (s Signature) getSignedHeaders(signedHeaders http.Header) string { var headers []string for k := range signedHeaders { headers = append(headers, strings.ToLower(k)) @@ -140,41 +117,6 @@ func (r Signature) getSignedHeaders(signedHeaders map[string][]string) string { return strings.Join(headers, ";") } -// extractSignedHeaders extract signed headers from Authorization header -func (r Signature) extractSignedHeaders() map[string][]string { - extractedSignedHeadersMap := make(map[string][]string) - for _, header := range r.SignedHeaders { - val, ok := r.Request.Header[http.CanonicalHeaderKey(header)] - if !ok { - // Golang http server strips off 'Expect' header, if the - // client sent this as part of signed headers we need to - // handle otherwise we would see a signature mismatch. - // `aws-cli` sets this as part of signed headers which is - // a bad idea since servers trying to implement AWS - // Signature version '4' will all encounter this issue. - // - // According to - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 - // Expect header is always of form: - // - // Expect = "Expect" ":" 1#expectation - // expectation = "100-continue" | expectation-extension - // - // So it safe to assume that '100-continue' is what would - // be sent, for the time being keep this work around. - // Adding a *TODO* to remove this later when Golang server - // doesn't filter out the 'Expect' header. - if header == "expect" { - extractedSignedHeadersMap[header] = []string{"100-continue"} - } - // if not found continue, we will fail later - continue - } - extractedSignedHeadersMap[header] = val - } - return extractedSignedHeadersMap -} - // getCanonicalRequest generate a canonical request of style // // canonicalRequest = @@ -185,18 +127,18 @@ func (r Signature) extractSignedHeaders() map[string][]string { // \n // // -func (r *Signature) getCanonicalRequest() string { - payload := r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-content-sha256")) - r.Request.URL.RawQuery = strings.Replace(r.Request.URL.Query().Encode(), "+", "%20", -1) - encodedPath := getURLEncodedName(r.Request.URL.Path) - // convert any space strings back to "+" +func (s *Signature) getCanonicalRequest() string { + payload := s.httpRequest.Header.Get(http.CanonicalHeaderKey("x-amz-content-sha256")) + s.httpRequest.URL.RawQuery = strings.Replace(s.httpRequest.URL.Query().Encode(), "+", "%20", -1) + encodedPath := getURLEncodedName(s.httpRequest.URL.Path) + // Convert any space strings back to "+". encodedPath = strings.Replace(encodedPath, "+", "%20", -1) canonicalRequest := strings.Join([]string{ - r.Request.Method, + s.httpRequest.Method, encodedPath, - r.Request.URL.RawQuery, - r.getCanonicalHeaders(r.extractSignedHeaders()), - r.getSignedHeaders(r.extractSignedHeaders()), + s.httpRequest.URL.RawQuery, + s.getCanonicalHeaders(s.extractedSignedHeaders), + s.getSignedHeaders(s.extractedSignedHeaders), payload, }, "\n") return canonicalRequest @@ -212,69 +154,89 @@ func (r *Signature) getCanonicalRequest() string { // \n // // -func (r Signature) getPresignedCanonicalRequest(presignedQuery string) string { +func (s Signature) getPresignedCanonicalRequest(presignedQuery string) string { rawQuery := strings.Replace(presignedQuery, "+", "%20", -1) - encodedPath := getURLEncodedName(r.Request.URL.Path) - // convert any space strings back to "+" + encodedPath := getURLEncodedName(s.httpRequest.URL.Path) + // Convert any space strings back to "+". encodedPath = strings.Replace(encodedPath, "+", "%20", -1) canonicalRequest := strings.Join([]string{ - r.Request.Method, + s.httpRequest.Method, encodedPath, rawQuery, - r.getCanonicalHeaders(r.extractSignedHeaders()), - r.getSignedHeaders(r.extractSignedHeaders()), + s.getCanonicalHeaders(s.extractedSignedHeaders), + s.getSignedHeaders(s.extractedSignedHeaders), "UNSIGNED-PAYLOAD", }, "\n") return canonicalRequest } -// getScope generate a string of a specific date, an AWS region, and a service -func (r Signature) getScope(t time.Time) string { +// getScope generate a string of a specific date, an AWS region, and a service. +func (s Signature) getScope(t time.Time) string { scope := strings.Join([]string{ t.Format(yyyymmdd), - r.Region, + s.region, "s3", "aws4_request", }, "/") return scope } -// getStringToSign a string based on selected query values -func (r Signature) getStringToSign(canonicalRequest string, t time.Time) string { - stringToSign := authHeaderPrefix + "\n" + t.Format(iso8601Format) + "\n" - stringToSign = stringToSign + r.getScope(t) + "\n" +// getStringToSign a string based on selected query values. +func (s Signature) getStringToSign(canonicalRequest string, t time.Time) string { + stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n" + stringToSign = stringToSign + s.getScope(t) + "\n" canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest)) stringToSign = stringToSign + hex.EncodeToString(canonicalRequestBytes[:]) return stringToSign } -// getSigningKey hmac seed to calculate final signature -func (r Signature) getSigningKey(t time.Time) []byte { - secret := r.SecretAccessKey +// getSigningKey hmac seed to calculate final signature. +func (s Signature) getSigningKey(t time.Time) []byte { + secret := s.secretAccessKey date := sumHMAC([]byte("AWS4"+secret), []byte(t.Format(yyyymmdd))) - region := sumHMAC(date, []byte(r.Region)) + region := sumHMAC(date, []byte(s.region)) service := sumHMAC(region, []byte("s3")) signingKey := sumHMAC(service, []byte("aws4_request")) return signingKey } -// getSignature final signature in hexadecimal form -func (r Signature) getSignature(signingKey []byte, stringToSign string) string { +// getSignature final signature in hexadecimal form. +func (s Signature) getSignature(signingKey []byte, stringToSign string) string { return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) } // DoesPolicySignatureMatch - Verify query headers with post policy // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html // returns true if matches, false otherwise. if error is not nil then it is always false -func (r *Signature) DoesPolicySignatureMatch(date string) (bool, *probe.Error) { - t, err := time.Parse(iso8601Format, date) +func (s *Signature) DoesPolicySignatureMatch(formValues map[string]string) (bool, *probe.Error) { + // Parse credential tag. + creds, err := parseCredential(formValues["X-Amz-Credential"]) if err != nil { - return false, probe.NewError(err) + return false, err.Trace(formValues["X-Amz-Credential"]) + } + + // Verify if the access key id matches. + if creds.accessKeyID != s.accessKeyID { + return false, ErrInvalidAccessKeyID("Access key id does not match with our records.", creds.accessKeyID).Trace(creds.accessKeyID) + } + + // Verify if the region is valid. + reqRegion := creds.scope.region + if !isValidRegion(reqRegion, s.region) { + return false, ErrInvalidRegion("Requested region is not recognized.", reqRegion).Trace(reqRegion) + } + + // Save region. + s.region = reqRegion + + // Parse date string. + t, e := time.Parse(iso8601Format, formValues["X-Amz-Date"]) + if e != nil { + return false, probe.NewError(e) } - signingKey := r.getSigningKey(t) - stringToSign := string(r.PresignedPolicy) - newSignature := r.getSignature(signingKey, stringToSign) - if newSignature != r.Signature { + signingKey := s.getSigningKey(t) + newSignature := s.getSignature(signingKey, formValues["Policy"]) + if newSignature != formValues["X-Amz-Signature"] { return false, nil } return true, nil @@ -283,35 +245,49 @@ func (r *Signature) DoesPolicySignatureMatch(date string) (bool, *probe.Error) { // DoesPresignedSignatureMatch - Verify query headers with presigned signature // - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html // returns true if matches, false otherwise. if error is not nil then it is always false -func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) { - query := make(url.Values) - query.Set("X-Amz-Algorithm", authHeaderPrefix) - - var date string - if date = r.Request.URL.Query().Get("X-Amz-Date"); date == "" { - return false, probe.NewError(MissingDateHeader{}) - } - t, err := time.Parse(iso8601Format, date) +func (s *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) { + // Parse request query string. + preSignV4Values, err := parsePreSignV4(s.httpRequest.URL.Query()) if err != nil { - return false, probe.NewError(err) + return false, err.Trace(s.httpRequest.URL.String()) } - if _, ok := r.Request.URL.Query()["X-Amz-Expires"]; !ok { - return false, probe.NewError(MissingExpiresQuery{}) + + // Verify if the access key id matches. + if preSignV4Values.Creds.accessKeyID != s.accessKeyID { + return false, ErrInvalidAccessKeyID("Access key id does not match with our records.", preSignV4Values.Creds.accessKeyID).Trace(preSignV4Values.Creds.accessKeyID) } - expireSeconds, err := strconv.Atoi(r.Request.URL.Query().Get("X-Amz-Expires")) - if err != nil { - return false, probe.NewError(err) + + // Verify if region is valid. + reqRegion := preSignV4Values.Creds.scope.region + if !isValidRegion(reqRegion, s.region) { + return false, ErrInvalidRegion("Requested region is not recognized.", reqRegion).Trace(reqRegion) } - if time.Now().UTC().Sub(t) > time.Duration(expireSeconds)*time.Second { - return false, probe.NewError(ExpiredPresignedRequest{}) + + // Save region. + s.region = reqRegion + + // Extract all the signed headers along with its values. + s.extractedSignedHeaders = extractSignedHeaders(preSignV4Values.SignedHeaders, s.httpRequest.Header) + + // Construct new query. + query := make(url.Values) + query.Set("X-Amz-Algorithm", signV4Algorithm) + + if time.Now().UTC().Sub(preSignV4Values.Date) > time.Duration(preSignV4Values.Expires)*time.Second { + return false, ErrExpiredPresignRequest("Presigned request already expired, please initiate a new request.") } + + // Save the date and expires. + t := preSignV4Values.Date + expireSeconds := int(preSignV4Values.Expires) + query.Set("X-Amz-Date", t.Format(iso8601Format)) 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)) + query.Set("X-Amz-SignedHeaders", s.getSignedHeaders(s.extractedSignedHeaders)) + query.Set("X-Amz-Credential", s.accessKeyID+"/"+s.getScope(t)) // Save other headers available in the request parameters. - for k, v := range r.Request.URL.Query() { + for k, v := range s.httpRequest.URL.Query() { if strings.HasPrefix(strings.ToLower(k), "x-amz") { continue } @@ -320,24 +296,24 @@ func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) { encodedQuery := query.Encode() // Verify if date query is same. - if r.Request.URL.Query().Get("X-Amz-Date") != query.Get("X-Amz-Date") { + if s.httpRequest.URL.Query().Get("X-Amz-Date") != query.Get("X-Amz-Date") { return false, nil } // Verify if expires query is same. - if r.Request.URL.Query().Get("X-Amz-Expires") != query.Get("X-Amz-Expires") { + if s.httpRequest.URL.Query().Get("X-Amz-Expires") != query.Get("X-Amz-Expires") { return false, nil } // Verify if signed headers query is same. - if r.Request.URL.Query().Get("X-Amz-SignedHeaders") != query.Get("X-Amz-SignedHeaders") { + if s.httpRequest.URL.Query().Get("X-Amz-SignedHeaders") != query.Get("X-Amz-SignedHeaders") { return false, nil } // Verify if credential query is same. - if r.Request.URL.Query().Get("X-Amz-Credential") != query.Get("X-Amz-Credential") { + if s.httpRequest.URL.Query().Get("X-Amz-Credential") != query.Get("X-Amz-Credential") { return false, nil } // Verify finally if signature is same. - newSignature := r.getSignature(r.getSigningKey(t), r.getStringToSign(r.getPresignedCanonicalRequest(encodedQuery), t)) - if r.Request.URL.Query().Get("X-Amz-Signature") != newSignature { + newSignature := s.getSignature(s.getSigningKey(t), s.getStringToSign(s.getPresignedCanonicalRequest(encodedQuery), t)) + if s.httpRequest.URL.Query().Get("X-Amz-Signature") != newSignature { return false, nil } return true, nil @@ -346,27 +322,57 @@ func (r *Signature) DoesPresignedSignatureMatch() (bool, *probe.Error) { // DoesSignatureMatch - Verify authorization header with calculated header in accordance with // - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html // returns true if matches, false otherwise. if error is not nil then it is always false -func (r *Signature) DoesSignatureMatch(hashedPayload string) (bool, *probe.Error) { - // set new calculated payload - r.Request.Header.Set("X-Amz-Content-Sha256", hashedPayload) +func (s *Signature) DoesSignatureMatch(hashedPayload string) (bool, *probe.Error) { + // Save authorization header. + v4Auth := s.httpRequest.Header.Get("Authorization") + + // Parse signature version '4' header. + signV4Values, err := parseSignV4(v4Auth) + if err != nil { + return false, err.Trace(v4Auth) + } + + // Extract all the signed headers along with its values. + s.extractedSignedHeaders = extractSignedHeaders(signV4Values.SignedHeaders, s.httpRequest.Header) + + // Verify if the access key id matches. + if signV4Values.Creds.accessKeyID != s.accessKeyID { + return false, ErrInvalidAccessKeyID("Access key id does not match with our records.", signV4Values.Creds.accessKeyID).Trace(signV4Values.Creds.accessKeyID) + } - // Add date if not present throw error + // Verify if region is valid. + reqRegion := signV4Values.Creds.scope.region + if !isValidRegion(reqRegion, s.region) { + return false, ErrInvalidRegion("Requested region is not recognized.", reqRegion).Trace(reqRegion) + } + + // Save region. + s.region = reqRegion + + // Set input payload. + s.httpRequest.Header.Set("X-Amz-Content-Sha256", hashedPayload) + + // Extract date, if not present throw error. var date string - if date = r.Request.Header.Get(http.CanonicalHeaderKey("x-amz-date")); date == "" { - if date = r.Request.Header.Get("Date"); date == "" { - return false, probe.NewError(MissingDateHeader{}) + if date = s.httpRequest.Header.Get(http.CanonicalHeaderKey("x-amz-date")); date == "" { + if date = s.httpRequest.Header.Get("Date"); date == "" { + return false, ErrMissingDateHeader("Date header is missing from the request.").Trace() } } - t, err := time.Parse(iso8601Format, date) - if err != nil { - return false, probe.NewError(err) + // Parse date header. + t, e := time.Parse(iso8601Format, date) + if e != nil { + return false, probe.NewError(e) } - canonicalRequest := r.getCanonicalRequest() - stringToSign := r.getStringToSign(canonicalRequest, t) - signingKey := r.getSigningKey(t) - newSignature := r.getSignature(signingKey, stringToSign) - if newSignature != r.Signature { + // Signature version '4'. + canonicalRequest := s.getCanonicalRequest() + stringToSign := s.getStringToSign(canonicalRequest, t) + signingKey := s.getSigningKey(t) + newSignature := s.getSignature(signingKey, stringToSign) + + // Verify if signature match. + if newSignature != signV4Values.Signature { return false, nil } return true, nil diff --git a/pkg/signature/utils.go b/pkg/signature/utils.go new file mode 100644 index 000000000..b05aea195 --- /dev/null +++ b/pkg/signature/utils.go @@ -0,0 +1,118 @@ +package signature + +import ( + "crypto/hmac" + "encoding/hex" + "net/http" + "regexp" + "strings" + "unicode/utf8" + + "github.com/minio/minio/pkg/crypto/sha256" +) + +// AccessID and SecretID length in bytes +const ( + MinioAccessID = 20 + MinioSecretID = 40 +) + +/// helpers + +// isValidSecretKey - validate secret key. +var isValidSecretKey = regexp.MustCompile("^.{40}$") + +// isValidAccessKey - validate access key. +var isValidAccessKey = regexp.MustCompile("^[A-Z0-9\\-\\.\\_\\~]{20}$") + +// isValidRegion - verify if incoming region value is valid with configured Region. +func isValidRegion(reqRegion string, confRegion string) bool { + if confRegion == "" || confRegion == "US" { + confRegion = "us-east-1" + } + // Some older s3 clients set region as "US" instead of + // "us-east-1", handle it. + if reqRegion == "US" { + reqRegion = "us-east-1" + } + return reqRegion == confRegion +} + +// sumHMAC calculate hmac between two input byte array. +func sumHMAC(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// getURLEncodedName encode the strings from UTF-8 byte representations to HTML hex escape sequences +// +// This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 +// non english characters cannot be parsed due to the nature in which url.Encode() is written +// +// This function on the other hand is a direct replacement for url.Encode() technique to support +// pretty much every UTF-8 character. +func getURLEncodedName(name string) string { + // if object matches reserved string, no need to encode them + reservedNames := regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") + if reservedNames.MatchString(name) { + return name + } + var encodedName string + for _, s := range name { + if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) + encodedName = encodedName + string(s) + continue + } + switch s { + case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) + encodedName = encodedName + string(s) + continue + default: + len := utf8.RuneLen(s) + if len < 0 { + return name + } + u := make([]byte, len) + utf8.EncodeRune(u, s) + for _, r := range u { + hex := hex.EncodeToString([]byte{r}) + encodedName = encodedName + "%" + strings.ToUpper(hex) + } + } + } + return encodedName +} + +// extractSignedHeaders extract signed headers from Authorization header +func extractSignedHeaders(signedHeaders []string, reqHeaders http.Header) http.Header { + extractedSignedHeaders := make(http.Header) + for _, header := range signedHeaders { + val, ok := reqHeaders[http.CanonicalHeaderKey(header)] + if !ok { + // Golang http server strips off 'Expect' header, if the + // client sent this as part of signed headers we need to + // handle otherwise we would see a signature mismatch. + // `aws-cli` sets this as part of signed headers. + // + // According to + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 + // Expect header is always of form: + // + // Expect = "Expect" ":" 1#expectation + // expectation = "100-continue" | expectation-extension + // + // So it safe to assume that '100-continue' is what would + // be sent, for the time being keep this work around. + // Adding a *TODO* to remove this later when Golang server + // doesn't filter out the 'Expect' header. + if header == "expect" { + extractedSignedHeaders[header] = []string{"100-continue"} + } + // If not found continue, we will fail later. + continue + } + extractedSignedHeaders[header] = val + } + return extractedSignedHeaders +} diff --git a/pkg/signature/v4-parser.go b/pkg/signature/v4-parser.go new file mode 100644 index 000000000..880f149cb --- /dev/null +++ b/pkg/signature/v4-parser.go @@ -0,0 +1,203 @@ +package signature + +import ( + "net/url" + "strings" + "time" + + "github.com/minio/minio/pkg/probe" +) + +type credScope struct { + accessKeyID string + scope struct { + date time.Time + region string + service string + request string + } +} + +func parseCredential(credElement string) (credScope, *probe.Error) { + creds := strings.Split(strings.TrimSpace(credElement), "=") + if len(creds) != 2 { + return credScope{}, ErrMissingFields("Credential tag has missing fields.", credElement).Trace(credElement) + } + if creds[0] != "Credential" { + return credScope{}, ErrMissingCredTag("Missing credentials tag.", credElement).Trace(credElement) + } + credElements := strings.Split(strings.TrimSpace(creds[1]), "/") + if len(credElements) != 5 { + return credScope{}, ErrCredMalformed("Credential values malformed.", credElement).Trace(credElement) + } + if !isValidAccessKey.MatchString(credElements[0]) { + return credScope{}, ErrInvalidAccessKeyID("Invalid access key id.", credElement).Trace(credElement) + } + cred := credScope{ + accessKeyID: credElements[0], + } + var e error + cred.scope.date, e = time.Parse(yyyymmdd, credElements[1]) + if e != nil { + return credScope{}, ErrInvalidDateFormat("Invalid date format.", credElement).Trace(credElement) + } + if credElements[2] == "" { + return credScope{}, ErrRegionISEmpty("Region is empty.", credElement).Trace(credElement) + } + cred.scope.region = credElements[2] + if credElements[3] != "s3" { + return credScope{}, ErrInvalidService("Invalid service detected.", credElement).Trace(credElement) + } + cred.scope.service = credElements[3] + if credElements[4] != "aws4_request" { + return credScope{}, ErrInvalidRequestVersion("Invalid request version detected.", credElement).Trace(credElement) + } + cred.scope.request = credElements[4] + return cred, nil +} + +// parse signature. +func parseSignature(signElement string) (string, *probe.Error) { + signFields := strings.Split(strings.TrimSpace(signElement), "=") + if len(signFields) != 2 { + return "", ErrMissingFields("Signature tag has missing fields.", signElement).Trace(signElement) + } + if signFields[0] != "Signature" { + return "", ErrMissingSignTag("Signature tag is missing", signElement).Trace(signElement) + } + signature := signFields[1] + return signature, nil +} + +// parse signed headers. +func parseSignedHeaders(signedHdrElement string) ([]string, *probe.Error) { + signedHdrFields := strings.Split(strings.TrimSpace(signedHdrElement), "=") + if len(signedHdrFields) != 2 { + return nil, ErrMissingFields("Signed headers tag has missing fields.", signedHdrElement).Trace(signedHdrElement) + } + if signedHdrFields[0] != "SignedHeaders" { + return nil, ErrMissingSignHeadersTag("Signed headers tag is missing.", signedHdrElement).Trace(signedHdrElement) + } + signedHeaders := strings.Split(signedHdrFields[1], ";") + return signedHeaders, nil +} + +// structured version of AWS Signature V4 header. +type signValues struct { + Creds credScope + SignedHeaders []string + Signature string +} + +// structued version of AWS Signature V4 query string. +type preSignValues struct { + signValues + Date time.Time + Expires time.Duration +} + +// Parses signature version '4' query string of the following form. +// +// querystring = X-Amz-Algorithm=algorithm +// querystring += &X-Amz-Credential= urlencode(access_key_ID + '/' + credential_scope) +// querystring += &X-Amz-Date=date +// querystring += &X-Amz-Expires=timeout interval +// querystring += &X-Amz-SignedHeaders=signed_headers +// querystring += &X-Amz-Signature=signature +// +func parsePreSignV4(query url.Values) (preSignValues, *probe.Error) { + // Verify if the query algorithm is supported or not. + if query.Get("X-Amz-Algorithm") != signV4Algorithm { + return preSignValues{}, ErrUnsuppSignAlgo("Unsupported algorithm in query string.", query.Get("X-Amz-Algorithm")) + } + + // Initialize signature version '4' structured header. + preSignV4Values := preSignValues{} + + var err *probe.Error + // Save credentail values. + preSignV4Values.Creds, err = parseCredential(query.Get("X-Amz-Credential")) + if err != nil { + return preSignValues{}, err.Trace(query.Get("X-Amz-Credential")) + } + + var e error + // Save date in native time.Time. + preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get("X-Amz-Date")) + if e != nil { + return preSignValues{}, ErrMalformedDate("Malformed date string.", query.Get("X-Amz-Date")).Trace(query.Get("X-Amz-Date")) + } + + // Save expires in native time.Duration. + preSignV4Values.Expires, e = time.ParseDuration(query.Get("X-Amz-Expires") + "s") + if e != nil { + return preSignValues{}, ErrMalformedExpires("Malformed expires string.", query.Get("X-Amz-Expires")).Trace(query.Get("X-Amz-Expires")) + } + + // Save signed headers. + preSignV4Values.SignedHeaders, err = parseSignedHeaders(query.Get("X-Amz-SignedHeaders")) + if err != nil { + return preSignValues{}, err.Trace(query.Get("X-Amz-SignedHeaders")) + } + + // Save signature. + preSignV4Values.Signature, err = parseSignature(query.Get("X-Amz-Signature")) + if err != nil { + return preSignValues{}, err.Trace(query.Get("X-Amz-Signature")) + } + + // Return structed form of signature query string. + return preSignV4Values, nil +} + +// Parses signature version '4' header of the following form. +// +// Authorization: algorithm Credential=access key ID/credential scope, \ +// SignedHeaders=SignedHeaders, Signature=signature +// +func parseSignV4(v4Auth string) (signValues, *probe.Error) { + // Replace all spaced strings, some clients can send spaced + // parameters and some won't. So we pro-actively remove any spaces + // to make parsing easier. + v4Auth = strings.Replace(v4Auth, " ", "", -1) + if v4Auth == "" { + return signValues{}, ErrAuthHeaderEmpty("Auth header empty.").Trace(v4Auth) + } + + // Verify if the header algorithm is supported or not. + if !strings.HasPrefix(v4Auth, signV4Algorithm) { + return signValues{}, ErrUnsuppSignAlgo("Unsupported algorithm in authorization header.", v4Auth).Trace(v4Auth) + } + + // Strip off the Algorithm prefix. + v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm) + authFields := strings.Split(strings.TrimSpace(v4Auth), ",") + if len(authFields) != 3 { + return signValues{}, ErrMissingFields("Missing fields in authorization header.", v4Auth).Trace(v4Auth) + } + + // Initialize signature version '4' structured header. + signV4Values := signValues{} + + var err *probe.Error + // Save credentail values. + signV4Values.Creds, err = parseCredential(authFields[0]) + if err != nil { + return signValues{}, err.Trace(v4Auth) + } + + // Save signed headers. + signV4Values.SignedHeaders, err = parseSignedHeaders(authFields[1]) + if err != nil { + return signValues{}, err.Trace(v4Auth) + } + + // Save signature. + signV4Values.Signature, err = parseSignature(authFields[2]) + if err != nil { + return signValues{}, err.Trace(v4Auth) + } + + // Return the structure here. + return signV4Values, nil +} diff --git a/pkg/xl/bucket.go b/pkg/xl/bucket.go index 850850ca3..645257e94 100644 --- a/pkg/xl/bucket.go +++ b/pkg/xl/bucket.go @@ -306,7 +306,7 @@ func (b bucket) WriteObject(objectName string, objectData io.Reader, size int64, // // Signature mismatch occurred all temp files to be removed and all data purged. CleanupWritersOnError(writers) - return ObjectMetadata{}, probe.NewError(signV4.SigDoesNotMatch{}) + return ObjectMetadata{}, probe.NewError(SignDoesNotMatch{}) } } objMetadata.MD5Sum = hex.EncodeToString(dataMD5sum) diff --git a/pkg/xl/errors.go b/pkg/xl/errors.go index 44726873f..75e59477c 100644 --- a/pkg/xl/errors.go +++ b/pkg/xl/errors.go @@ -18,6 +18,13 @@ package xl import "fmt" +// SignDoesNotMatch - signature does not match. +type SignDoesNotMatch struct{} + +func (e SignDoesNotMatch) Error() string { + return "Signature does not match." +} + // InvalidArgument invalid argument type InvalidArgument struct{} diff --git a/pkg/xl/multipart.go b/pkg/xl/multipart.go index 0588c0816..7349fa925 100644 --- a/pkg/xl/multipart.go +++ b/pkg/xl/multipart.go @@ -226,7 +226,7 @@ func (xl API) createObjectPart(bucket, key, uploadID string, partID int, content return "", err.Trace() } if !ok { - return "", probe.NewError(signV4.SigDoesNotMatch{}) + return "", probe.NewError(SignDoesNotMatch{}) } } } @@ -342,7 +342,7 @@ func (xl API) completeMultipartUploadV2(bucket, key, uploadID string, data io.Re return nil, err.Trace() } if !ok { - return nil, probe.NewError(signV4.SigDoesNotMatch{}) + return nil, probe.NewError(SignDoesNotMatch{}) } } parts := &CompleteMultipartUpload{} diff --git a/pkg/xl/xl-v1.go b/pkg/xl/xl-v1.go index 14cbfeaaa..613684a8f 100644 --- a/pkg/xl/xl-v1.go +++ b/pkg/xl/xl-v1.go @@ -376,7 +376,7 @@ func (xl API) completeMultipartUpload(bucket, object, uploadID string, data io.R return ObjectMetadata{}, err.Trace() } if !ok { - return ObjectMetadata{}, probe.NewError(signV4.SigDoesNotMatch{}) + return ObjectMetadata{}, probe.NewError(SignDoesNotMatch{}) } } parts := &CompleteMultipartUpload{} diff --git a/pkg/xl/xl-v2.go b/pkg/xl/xl-v2.go index 9d832f798..962943b46 100644 --- a/pkg/xl/xl-v2.go +++ b/pkg/xl/xl-v2.go @@ -392,7 +392,7 @@ func (xl API) createObject(bucket, key, contentType, expectedMD5Sum string, size if !ok { // Delete perhaps the object is already saved, due to the nature of append() xl.objects.Delete(objectKey) - return ObjectMetadata{}, probe.NewError(signV4.SigDoesNotMatch{}) + return ObjectMetadata{}, probe.NewError(SignDoesNotMatch{}) } } @@ -435,7 +435,7 @@ func (xl API) MakeBucket(bucketName, acl string, location io.Reader, signature * return err.Trace() } if !ok { - return probe.NewError(signV4.SigDoesNotMatch{}) + return probe.NewError(SignDoesNotMatch{}) } } diff --git a/routers.go b/routers.go index ab4b1d58b..420014296 100644 --- a/routers.go +++ b/routers.go @@ -26,6 +26,7 @@ import ( "github.com/minio/minio-go" "github.com/minio/minio/pkg/fs" "github.com/minio/minio/pkg/probe" + signV4 "github.com/minio/minio/pkg/signature" ) // CloudStorageAPI container for S3 compatible API. @@ -34,6 +35,10 @@ type CloudStorageAPI struct { AccessLog bool // Filesystem instance. Filesystem fs.Filesystem + // Signature instance. + Signature *signV4.Signature + // Region instance. + Region string } // WebAPI container for Web API. @@ -57,7 +62,6 @@ func getWebAPIHandler(web *WebAPI) http.Handler { var handlerFns = []HandlerFunc{ setCacheControlHandler, // Adds Cache-Control header setTimeValidityHandler, // Validate time. - setJWTAuthHandler, // Authentication handler for verifying JWT's. setCorsHandler, // CORS added only for testing purposes. } if web.AccessLog { @@ -146,9 +150,14 @@ func getNewCloudStorageAPI(conf cloudServerConfig) CloudStorageAPI { fs, err := fs.New(conf.Path, conf.MinFreeDisk) fatalIf(err.Trace(), "Initializing filesystem failed.", nil) + sign, err := signV4.New(conf.AccessKeyID, conf.SecretAccessKey, conf.Region) + fatalIf(err.Trace(conf.AccessKeyID, conf.SecretAccessKey, conf.Region), "Initializing signature version '4' failed.", nil) + return CloudStorageAPI{ - Filesystem: fs, AccessLog: conf.AccessLog, + Filesystem: fs, + Signature: sign, + Region: conf.Region, } } @@ -157,7 +166,7 @@ func getCloudStorageAPIHandler(api CloudStorageAPI) http.Handler { setTimeValidityHandler, setIgnoreResourcesHandler, setIgnoreSignatureV2RequestHandler, - setSignatureHandler, + setAuthHandler, } if api.AccessLog { handlerFns = append(handlerFns, setAccessLogHandler) diff --git a/server-main.go b/server-main.go index f02374742..505289027 100644 --- a/server-main.go +++ b/server-main.go @@ -77,6 +77,7 @@ type cloudServerConfig struct { // Credentials. AccessKeyID string // Access key id. SecretAccessKey string // Secret access key. + Region string // Region string. /// FS options Path string // Path to export for cloud storage @@ -299,12 +300,17 @@ func serverMain(c *cli.Context) { if _, err := os.Stat(path); err != nil { fatalIf(probe.NewError(err), "Unable to validate the path", nil) } + region := conf.Credentials.Region + if region == "" { + region = "us-east-1" + } tls := (certFile != "" && keyFile != "") serverConfig := cloudServerConfig{ Address: c.GlobalString("address"), AccessLog: c.GlobalBool("enable-accesslog"), AccessKeyID: conf.Credentials.AccessKeyID, SecretAccessKey: conf.Credentials.SecretAccessKey, + Region: region, Path: path, MinFreeDisk: minFreeDisk, TLS: tls, diff --git a/server_fs_test.go b/server_fs_test.go index 9aa97d33d..85fc24d58 100644 --- a/server_fs_test.go +++ b/server_fs_test.go @@ -77,8 +77,11 @@ func (s *MyAPIFSCacheSuite) SetUpSuite(c *C) { c.Assert(saveConfig(conf), IsNil) cloudServer := cloudServerConfig{ - Path: fsroot, - MinFreeDisk: 0, + Path: fsroot, + MinFreeDisk: 0, + AccessKeyID: s.accessKeyID, + SecretAccessKey: s.secretAccessKey, + Region: "us-east-1", } cloudStorageAPI := getNewCloudStorageAPI(cloudServer) httpHandler := getCloudStorageAPIHandler(cloudStorageAPI) @@ -225,7 +228,7 @@ func (s *MyAPIFSCacheSuite) newRequest(method, urlStr string, contentLength int6 "aws4_request", }, "/") - stringToSign := authHeaderPrefix + "\n" + t.Format(iso8601Format) + "\n" + stringToSign := "AWS4-HMAC-SHA256" + "\n" + t.Format(iso8601Format) + "\n" stringToSign = stringToSign + scope + "\n" stringToSign = stringToSign + hex.EncodeToString(sum256([]byte(canonicalRequest))) @@ -238,7 +241,7 @@ func (s *MyAPIFSCacheSuite) newRequest(method, urlStr string, contentLength int6 // final Authorization header parts := []string{ - authHeaderPrefix + " Credential=" + s.accessKeyID + "/" + scope, + "AWS4-HMAC-SHA256" + " Credential=" + s.accessKeyID + "/" + scope, "SignedHeaders=" + signedHeaders, "Signature=" + signature, } diff --git a/signature-handler.go b/signature-handler.go deleted file mode 100644 index 1b20c8a19..000000000 --- a/signature-handler.go +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Minio Cloud 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 main - -import ( - "encoding/hex" - "net/http" - "strings" - - "github.com/minio/minio/pkg/crypto/sha256" - "github.com/minio/minio/pkg/probe" - v4 "github.com/minio/minio/pkg/signature" -) - -type signatureHandler struct { - handler http.Handler -} - -// setSignatureHandler to validate authorization header for the incoming request. -func setSignatureHandler(h http.Handler) http.Handler { - return signatureHandler{h} -} - -func isRequestSignatureV4(req *http.Request) bool { - if _, ok := req.Header["Authorization"]; ok { - if strings.HasPrefix(req.Header.Get("Authorization"), authHeaderPrefix) { - return ok - } - } - return false -} - -func isRequestRequiresACLCheck(req *http.Request) bool { - if isRequestSignatureV4(req) || isRequestPresignedSignatureV4(req) || isRequestPostPolicySignatureV4(req) { - return false - } - return true -} - -func isRequestPresignedSignatureV4(req *http.Request) bool { - if _, ok := req.URL.Query()["X-Amz-Credential"]; ok { - return ok - } - return false -} - -func isRequestPostPolicySignatureV4(req *http.Request) bool { - if _, ok := req.Header["Content-Type"]; ok { - if strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data") { - return true - } - } - return false -} - -func (s signatureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if isRequestPostPolicySignatureV4(r) && r.Method == "POST" { - s.handler.ServeHTTP(w, r) - return - } - var signature *v4.Signature - if isRequestSignatureV4(r) { - // For PUT and POST requests with payload, send the call upwards for verification. - // Or PUT and POST requests without payload, verify here. - if (r.Body == nil && (r.Method == "PUT" || r.Method == "POST")) || (r.Method != "PUT" && r.Method != "POST") { - // Init signature V4 verification - var err *probe.Error - signature, err = initSignatureV4(r) - if err != nil { - switch err.ToGoError() { - case errInvalidRegion: - errorIf(err.Trace(), "Unknown region in authorization header.", nil) - writeErrorResponse(w, r, AuthorizationHeaderMalformed, r.URL.Path) - return - case errAccessKeyIDInvalid: - errorIf(err.Trace(), "Invalid access key id.", nil) - writeErrorResponse(w, r, InvalidAccessKeyID, r.URL.Path) - return - default: - errorIf(err.Trace(), "Initializing signature v4 failed.", nil) - writeErrorResponse(w, r, InternalError, r.URL.Path) - return - } - } - dummySha256Bytes := sha256.Sum256([]byte("")) - ok, err := signature.DoesSignatureMatch(hex.EncodeToString(dummySha256Bytes[:])) - if err != nil { - errorIf(err.Trace(), "Unable to verify signature.", nil) - writeErrorResponse(w, r, InternalError, r.URL.Path) - return - } - if !ok { - writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) - return - } - } - s.handler.ServeHTTP(w, r) - return - } - if isRequestPresignedSignatureV4(r) { - var err *probe.Error - signature, err = initPresignedSignatureV4(r) - if err != nil { - switch err.ToGoError() { - case errAccessKeyIDInvalid: - errorIf(err.Trace(), "Invalid access key id requested.", nil) - writeErrorResponse(w, r, InvalidAccessKeyID, r.URL.Path) - return - default: - errorIf(err.Trace(), "Initializing signature v4 failed.", nil) - writeErrorResponse(w, r, InternalError, r.URL.Path) - return - } - } - ok, err := signature.DoesPresignedSignatureMatch() - if err != nil { - errorIf(err.Trace(), "Unable to verify signature.", nil) - writeErrorResponse(w, r, InternalError, r.URL.Path) - return - } - if !ok { - writeErrorResponse(w, r, SignatureDoesNotMatch, r.URL.Path) - return - } - s.handler.ServeHTTP(w, r) - return - } - // call goes up from here, let ACL's verify the validity of the request - s.handler.ServeHTTP(w, r) -} diff --git a/signature.go b/signature.go new file mode 100644 index 000000000..59626d13e --- /dev/null +++ b/signature.go @@ -0,0 +1,73 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "net/http" + "strings" + + signV4 "github.com/minio/minio/pkg/signature" +) + +func isRequestJWT(r *http.Request) bool { + if _, ok := r.Header["Authorization"]; ok { + if strings.HasPrefix(r.Header.Get("Authorization"), jwtAlgorithm) { + return ok + } + } + return false +} + +func isRequestSignatureV4(r *http.Request) bool { + if _, ok := r.Header["Authorization"]; ok { + if strings.HasPrefix(r.Header.Get("Authorization"), signV4Algorithm) { + return ok + } + } + return false +} + +func isRequestPresignedSignatureV4(r *http.Request) bool { + if _, ok := r.URL.Query()["X-Amz-Credential"]; ok { + return ok + } + return false +} + +func isRequestPostPolicySignatureV4(r *http.Request) bool { + if _, ok := r.Header["Content-Type"]; ok { + if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") { + return true + } + } + return false +} + +func isRequestRequiresACLCheck(r *http.Request) bool { + if isRequestSignatureV4(r) || isRequestPresignedSignatureV4(r) || isRequestPostPolicySignatureV4(r) { + return false + } + return true +} + +func isSignV4ReqAuthenticated(sign *signV4.Signature, r *http.Request) bool { + auth := sign.SetHTTPRequestToVerify(r) + if isRequestSignatureV4(r) { + dummyPayload := sha256.Sum256([]byte("")) + ok, err := auth.DoesSignatureMatch(hex.EncodeToString(dummyPayload[:])) + if err != nil { + errorIf(err.Trace(), "Signature verification failed.", nil) + return false + } + return ok + } + if isRequestPresignedSignatureV4(r) { + ok, err := auth.DoesPresignedSignatureMatch() + if err != nil { + errorIf(err.Trace(), "Presigned signature verification failed.", nil) + return false + } + return ok + } + return false +} diff --git a/web-handlers.go b/web-handlers.go index b79aeeffc..eb8bff393 100644 --- a/web-handlers.go +++ b/web-handlers.go @@ -36,9 +36,9 @@ import ( "github.com/minio/minio/pkg/probe" ) -// isAuthenticated validates if any incoming request to be a valid JWT +// isJWTReqAuthencatied validates if any incoming request to be a valid JWT // authenticated request. -func isAuthenticated(req *http.Request) bool { +func isJWTReqAuthencatied(req *http.Request) bool { jwt := InitJWT() tokenRequest, e := jwtgo.ParseFromRequest(req, func(token *jwtgo.Token) (interface{}, error) { if _, ok := token.Method.(*jwtgo.SigningMethodHMAC); !ok { @@ -60,7 +60,7 @@ func (web WebAPI) GetUIVersion(r *http.Request, args *GenericArgs, reply *Generi // ServerInfo - get server info. func (web *WebAPI) ServerInfo(r *http.Request, args *ServerInfoArgs, reply *ServerInfoRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } host, err := os.Hostname() @@ -89,7 +89,7 @@ func (web *WebAPI) ServerInfo(r *http.Request, args *ServerInfoArgs, reply *Serv // DiskInfo - get disk statistics. func (web *WebAPI) DiskInfo(r *http.Request, args *DiskInfoArgs, reply *DiskInfoRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } info, e := disk.GetInfo(web.FSPath) @@ -103,7 +103,7 @@ func (web *WebAPI) DiskInfo(r *http.Request, args *DiskInfoArgs, reply *DiskInfo // MakeBucket - make a bucket. func (web *WebAPI) MakeBucket(r *http.Request, args *MakeBucketArgs, reply *GenericRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } reply.UIVersion = uiVersion @@ -116,7 +116,7 @@ func (web *WebAPI) MakeBucket(r *http.Request, args *MakeBucketArgs, reply *Gene // ListBuckets - list buckets api. func (web *WebAPI) ListBuckets(r *http.Request, args *ListBucketsArgs, reply *ListBucketsRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } buckets, e := web.Client.ListBuckets() @@ -135,7 +135,7 @@ func (web *WebAPI) ListBuckets(r *http.Request, args *ListBucketsArgs, reply *Li // ListObjects - list objects api. func (web *WebAPI) ListObjects(r *http.Request, args *ListObjectsArgs, reply *ListObjectsRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } doneCh := make(chan struct{}) @@ -183,7 +183,7 @@ func getTargetHost(apiAddress, targetHost string) (string, *probe.Error) { // PutObjectURL - generates url for upload access. func (web *WebAPI) PutObjectURL(r *http.Request, args *PutObjectURLArgs, reply *PutObjectURLRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } targetHost, err := getTargetHost(web.apiAddress, args.TargetHost) @@ -205,7 +205,7 @@ func (web *WebAPI) PutObjectURL(r *http.Request, args *PutObjectURLArgs, reply * // GetObjectURL - generates url for download access. func (web *WebAPI) GetObjectURL(r *http.Request, args *GetObjectURLArgs, reply *GetObjectURLRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } @@ -237,7 +237,7 @@ func (web *WebAPI) GetObjectURL(r *http.Request, args *GetObjectURLArgs, reply * // RemoveObject - removes an object. func (web *WebAPI) RemoveObject(r *http.Request, args *RemoveObjectArgs, reply *GenericRep) error { - if !isAuthenticated(r) { + if !isJWTReqAuthencatied(r) { return &json2.Error{Message: "Unauthorized request"} } reply.UIVersion = uiVersion