diff --git a/pkg/api/minioapi/minioapi.go b/pkg/api/minioapi/minioapi.go index 6fb47c2bf..1af306774 100644 --- a/pkg/api/minioapi/minioapi.go +++ b/pkg/api/minioapi/minioapi.go @@ -23,10 +23,13 @@ import ( "log" "net/http" "strconv" + "strings" "time" "github.com/gorilla/mux" mstorage "github.com/minio-io/minio/pkg/storage" + "github.com/minio-io/minio/pkg/utils/config" + "github.com/minio-io/minio/pkg/utils/crypto/signers" ) type contentType int @@ -44,6 +47,11 @@ type minioApi struct { storage mstorage.Storage } +type vHandler struct { + conf config.Config + handler http.Handler +} + // No encoder interface exists, so we create one. type encoder interface { Encode(v interface{}) error @@ -54,6 +62,11 @@ func HttpHandler(storage mstorage.Storage) http.Handler { var api = minioApi{} api.storage = storage + var conf = config.Config{} + if err := conf.SetupConfig(); err != nil { + log.Fatal(err) + } + // Re-direct /path to /path/ mux.StrictSlash(true) mux.HandleFunc("/", api.listBucketsHandler).Methods("GET") @@ -63,7 +76,50 @@ func HttpHandler(storage mstorage.Storage) http.Handler { mux.HandleFunc("/{bucket}/{object:.*}", api.headObjectHandler).Methods("HEAD") mux.HandleFunc("/{bucket}/{object:.*}", api.putObjectHandler).Methods("PUT") - return ignoreUnimplementedResources(mux) + return validateHandler(conf, ignoreUnimplementedResources(mux)) +} + +// grab AccessKey from authorization header +func stripAccessKey(r *http.Request) string { + fields := strings.Fields(r.Header.Get("Authorization")) + if len(fields) < 2 { + return "" + } + splits := strings.Split(fields[1], ":") + if len(splits) < 2 { + return "" + } + return splits[0] +} + +func validateHandler(conf config.Config, h http.Handler) http.Handler { + return vHandler{conf, h} +} + +func (h vHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + accessKey := stripAccessKey(r) + if accessKey != "" { + if err := h.conf.ReadConfig(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } else { + user := h.conf.GetKey(accessKey) + ok, err := signers.ValidateRequest(user, r) + if ok { + h.handler.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(err.Error())) + } + } + } else { + //No access key found, handle this more appropriately + //TODO: Remove this after adding tests to support signature + //request + h.handler.ServeHTTP(w, r) + //Add this line, to reply back for invalid requests + //w.WriteHeader(http.StatusUnauthorized) + //w.Write([]byte("Authorization header malformed") + } } func ignoreUnimplementedResources(h http.Handler) http.Handler { diff --git a/pkg/utils/config/config.go b/pkg/utils/config/config.go index daf5fc49c..366156d61 100644 --- a/pkg/utils/config/config.go +++ b/pkg/utils/config/config.go @@ -55,6 +55,14 @@ func (c *Config) IsUserExists(username string) bool { return false } +func (c *Config) GetKey(accessKey string) User { + value, ok := c.Users[accessKey] + if !ok { + return User{} + } + return value +} + func (c *Config) GetUser(username string) User { for _, user := range c.Users { if user.Name == username { diff --git a/pkg/utils/crypto/keys/common.go b/pkg/utils/crypto/keys/common.go index 9a13a0126..21a3270f1 100644 --- a/pkg/utils/crypto/keys/common.go +++ b/pkg/utils/crypto/keys/common.go @@ -8,3 +8,21 @@ const ( func isalnum(c byte) bool { return '0' <= c && c <= '9' || 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' } + +func ValidateAccessKey(key []byte) bool { + for _, char := range key { + if isalnum(char) { + continue + } + switch char { + case '-': + case '.': + case '_': + case '~': + continue + default: + return false + } + } + return true +} diff --git a/pkg/utils/crypto/signers/signers.go b/pkg/utils/crypto/signers/signers.go index 2b9a5ca9d..f7cc473ba 100644 --- a/pkg/utils/crypto/signers/signers.go +++ b/pkg/utils/crypto/signers/signers.go @@ -1,191 +1,155 @@ package signers -//import ( -// "bytes" -// "crypto/hmac" -// "crypto/sha1" -// "encoding/base64" -// "fmt" -// "io" -// "net/http" -// "net/url" -// "sort" -// "strings" -// -// "github.com/minio-io/minio/pkg/utils/database" -//) -// -//func ValidateAccessKey(key []byte) bool { -// for _, char := range key { -// if isalnum(char) { -// continue -// } -// switch char { -// case '-': -// case '.': -// case '_': -// case '~': -// continue -// default: -// return false -// } -// } -// return true -//} -// -//func getAccessID() { -//} -// -//func getSecretID() { -//} -// -//// This package implements verification side of Object API Signature request -//func ValidateRequest(req *http.Request) (bool, error) { -// if date := req.Header.Get("Date"); date == "" { -// return false, fmt.Errorf("Date should be set") -// } -// hm := hmac.New(sha1.New, []byte(SecretAccessKey)) -// ss := getStringToSign(req) -// io.WriteString(hm, ss) -// authHeader := new(bytes.Buffer) -// if req.Header.Get("User-Agent") == "Minio" { -// fmt.Fprintf(authHeader, "MINIO %s:", AccessKey) -// } else { -// fmt.Fprintf(authHeader, "AWS %s:", AccessKey) -// } -// encoder := base64.NewEncoder(base64.StdEncoding, authHeader) -// encoder.Write(hm.Sum(nil)) -// defer encoder.Close() -// if req.Header.Get("Authorization") != authHeader.String() { -// return false, fmt.Errorf("Authorization header mismatch") -// } -// return true, nil -//} -// -//// From the Amazon docs: -//// -//// StringToSign = HTTP-Verb + "\n" + -//// Content-MD5 + "\n" + -//// Content-Type + "\n" + -//// Date + "\n" + -//// CanonicalizedAmzHeaders + -//// CanonicalizedResource; -//func getStringToSign(req *http.Request) string { -// buf := new(bytes.Buffer) -// buf.WriteString(req.Method) -// buf.WriteByte('\n') -// buf.WriteString(req.Header.Get("Content-MD5")) -// buf.WriteByte('\n') -// buf.WriteString(req.Header.Get("Content-Type")) -// buf.WriteByte('\n') -// if req.Header.Get("x-amz-date") == "" { -// buf.WriteString(req.Header.Get("Date")) -// } -// buf.WriteByte('\n') -// writeCanonicalizedAmzHeaders(buf, req) -// writeCanonicalizedResource(buf, req) -// return buf.String() -//} -// -//func hasPrefixCaseInsensitive(s, pfx string) bool { -// if len(pfx) > len(s) { -// return false -// } -// shead := s[:len(pfx)] -// if shead == pfx { -// return true -// } -// shead = strings.ToLower(shead) -// return shead == pfx || shead == strings.ToLower(pfx) -//} -// -//func writeCanonicalizedAmzHeaders(buf *bytes.Buffer, req *http.Request) { -// amzHeaders := make([]string, 0) -// vals := make(map[string][]string) -// for k, vv := range req.Header { -// if hasPrefixCaseInsensitive(k, "x-amz-") { -// lk := strings.ToLower(k) -// amzHeaders = append(amzHeaders, lk) -// vals[lk] = vv -// } -// } -// sort.Strings(amzHeaders) -// for _, k := range amzHeaders { -// buf.WriteString(k) -// buf.WriteByte(':') -// for idx, v := range vals[k] { -// if idx > 0 { -// buf.WriteByte(',') -// } -// if strings.Contains(v, "\n") { -// // TODO: "Unfold" long headers that -// // span multiple lines (as allowed by -// // RFC 2616, section 4.2) by replacing -// // the folding white-space (including -// // new-line) by a single space. -// buf.WriteString(v) -// } else { -// buf.WriteString(v) -// } -// } -// buf.WriteByte('\n') -// } -//} -// -//// Must be sorted: -//var subResList = []string{"acl", "lifecycle", "location", "logging", "notification", "partNumber", "policy", "requestPayment", "torrent", "uploadId", "uploads", "versionId", "versioning", "versions", "website"} -// -//// From the Amazon docs: -//// -//// CanonicalizedResource = [ "/" + Bucket ] + -//// + -//// [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; -//func writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) { -// bucket := getBucketFromHostname(req) -// if bucket != "" { -// buf.WriteByte('/') -// buf.WriteString(bucket) -// } -// buf.WriteString(req.URL.Path) -// if req.URL.RawQuery != "" { -// n := 0 -// vals, _ := url.ParseQuery(req.URL.RawQuery) -// for _, subres := range subResList { -// if vv, ok := vals[subres]; ok && len(vv) > 0 { -// n++ -// if n == 1 { -// buf.WriteByte('?') -// } else { -// buf.WriteByte('&') -// } -// buf.WriteString(subres) -// if len(vv[0]) > 0 { -// buf.WriteByte('=') -// buf.WriteString(url.QueryEscape(vv[0])) -// } -// } -// } -// } -//} +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "github.com/minio-io/minio/pkg/utils/config" +) + +func SignRequest(user config.User, req *http.Request) { + if date := req.Header.Get("Date"); date == "" { + req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) + } + hm := hmac.New(sha1.New, []byte(user.SecretKey)) + ss := getStringToSign(req) + io.WriteString(hm, ss) + + authHeader := new(bytes.Buffer) + fmt.Fprintf(authHeader, "AWS %s:", user.AccessKey) + encoder := base64.NewEncoder(base64.StdEncoding, authHeader) + encoder.Write(hm.Sum(nil)) + encoder.Close() + req.Header.Set("Authorization", authHeader.String()) +} + +// This package implements verification side of Object API Signature request +func ValidateRequest(user config.User, req *http.Request) (bool, error) { + if date := req.Header.Get("Date"); date == "" { + return false, fmt.Errorf("Date should be set") + } + hm := hmac.New(sha1.New, []byte(user.SecretKey)) + ss := getStringToSign(req) + io.WriteString(hm, ss) + + authHeader := new(bytes.Buffer) + fmt.Fprintf(authHeader, "AWS %s:", user.AccessKey) + encoder := base64.NewEncoder(base64.StdEncoding, authHeader) + encoder.Write(hm.Sum(nil)) + encoder.Close() + + if req.Header.Get("Authorization") != authHeader.String() { + return false, fmt.Errorf("Authorization header mismatch") + } + return true, nil +} + +// From the Amazon docs: // -//// hasDotSuffix reports whether s ends with "." + suffix. -//func hasDotSuffix(s string, suffix string) bool { -// return len(s) >= len(suffix)+1 && strings.HasSuffix(s, suffix) && s[len(s)-len(suffix)-1] == '.' -//} +// StringToSign = HTTP-Verb + "\n" + +// Content-MD5 + "\n" + +// Content-Type + "\n" + +// Date + "\n" + +// CanonicalizedAmzHeaders + +// CanonicalizedResource; +func getStringToSign(req *http.Request) string { + buf := new(bytes.Buffer) + buf.WriteString(req.Method) + buf.WriteByte('\n') + buf.WriteString(req.Header.Get("Content-MD5")) + buf.WriteByte('\n') + buf.WriteString(req.Header.Get("Content-Type")) + buf.WriteByte('\n') + if req.Header.Get("x-amz-date") == "" { + buf.WriteString(req.Header.Get("Date")) + } + buf.WriteByte('\n') + writeCanonicalizedAmzHeaders(buf, req) + writeCanonicalizedResource(buf, req) + return buf.String() +} + +func hasPrefixCaseInsensitive(s, pfx string) bool { + if len(pfx) > len(s) { + return false + } + shead := s[:len(pfx)] + if shead == pfx { + return true + } + shead = strings.ToLower(shead) + return shead == pfx || shead == strings.ToLower(pfx) +} + +func writeCanonicalizedAmzHeaders(buf *bytes.Buffer, req *http.Request) { + amzHeaders := make([]string, 0) + vals := make(map[string][]string) + for k, vv := range req.Header { + if hasPrefixCaseInsensitive(k, "x-amz-") { + lk := strings.ToLower(k) + amzHeaders = append(amzHeaders, lk) + vals[lk] = vv + } + } + sort.Strings(amzHeaders) + for _, k := range amzHeaders { + buf.WriteString(k) + buf.WriteByte(':') + for idx, v := range vals[k] { + if idx > 0 { + buf.WriteByte(',') + } + if strings.Contains(v, "\n") { + // TODO: "Unfold" long headers that + // span multiple lines (as allowed by + // RFC 2616, section 4.2) by replacing + // the folding white-space (including + // new-line) by a single space. + buf.WriteString(v) + } else { + buf.WriteString(v) + } + } + buf.WriteByte('\n') + } +} + +// Must be sorted: +var subResList = []string{"acl", "lifecycle", "location", "logging", "notification", "partNumber", "policy", "requestPayment", "torrent", "uploadId", "uploads", "versionId", "versioning", "versions", "website"} + +// From the Amazon docs: // -//func getBucketFromHostname(req *http.Request) string { -// host := req.Host -// if host == "" { -// host = req.URL.Host -// } -// if host == "s3.amazonaws.com" { -// return "" -// } -// if hostSuffix := "s3.amazonaws.com"; hasDotSuffix(host, hostSuffix) { -// return host[:len(host)-len(hostSuffix)-1] -// } -// if lastColon := strings.LastIndex(host, ":"); lastColon != -1 { -// return host[:lastColon] -// } -// return host -//} +// CanonicalizedResource = [ "/" + Bucket ] + +// + +// [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"]; +func writeCanonicalizedResource(buf *bytes.Buffer, req *http.Request) { + buf.WriteString(req.URL.Path) + if req.URL.RawQuery != "" { + n := 0 + vals, _ := url.ParseQuery(req.URL.RawQuery) + for _, subres := range subResList { + if vv, ok := vals[subres]; ok && len(vv) > 0 { + n++ + if n == 1 { + buf.WriteByte('?') + } else { + buf.WriteByte('&') + } + buf.WriteString(subres) + if len(vv[0]) > 0 { + buf.WriteByte('=') + buf.WriteString(url.QueryEscape(vv[0])) + } + } + } + } +}