From 188715df2458463f70c931e112588812e4276582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9C=8D=E6=99=93=E6=A0=8B?= Date: Sat, 31 Dec 2016 11:56:14 +0800 Subject: [PATCH] add another filer authentication support --- weed/security/guard.go | 260 ++++++++++++++++++++++++++++++++++- weed/server/filer_server.go | 7 + weed/server/master_server.go | 2 +- weed/server/volume_server.go | 2 +- 4 files changed, 264 insertions(+), 7 deletions(-) diff --git a/weed/security/guard.go b/weed/security/guard.go index dea3b12f2..7ee9bb0a7 100644 --- a/weed/security/guard.go +++ b/weed/security/guard.go @@ -1,10 +1,16 @@ package security import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "errors" "fmt" "net" "net/http" + "net/url" + "sort" "strings" "github.com/chrislusf/seaweedfs/weed/glog" @@ -40,16 +46,52 @@ Referenced: https://github.com/pkieltyka/jwtauth/blob/master/jwtauth.go */ + +/* + +Below sample config section should be modified per your needs and added into the filer.json config file if you want to +enable filer R/W API verfication like the AWS S3 RESTFUL authentication + +{ + "appInfo": [ + { + "AppKeyId": "XVNVWEYQVNGXWWNXGLKP", + "AppKeySecret": "H8peDqD3A9Uc8KO28Iu99ywnW7TKQ8pii2PE8xpQ", + "bucketList": [ + "yourbucketName" + ] + } + ] +} + +*/ +type AppContent struct { + AppKeySecret string + BucketList []string +} +type AppConf struct { + AppContent + AppKeyID string +} + type Guard struct { - whiteList []string - SecretKey Secret + whiteList []string + SecretKey Secret + appKeyDict map[string]AppContent isActive bool } -func NewGuard(whiteList []string, secretKey string) *Guard { - g := &Guard{whiteList: whiteList, SecretKey: Secret(secretKey)} - g.isActive = len(g.whiteList) != 0 || len(g.SecretKey) != 0 +func NewGuard(whiteList []string, secretKey string, appConfs []AppConf) *Guard { + appKeyDict := make(map[string]AppContent, len(appConfs)) + for _, appConf := range appConfs { + appKeyDict[appConf.AppKeyID] = AppContent{ + AppKeySecret: appConf.AppKeySecret, + BucketList: appConf.BucketList, + } + } + g := &Guard{whiteList: whiteList, SecretKey: Secret(secretKey), appKeyDict: appKeyDict} + g.isActive = len(g.whiteList) != 0 || len(g.SecretKey) != 0 || len(g.appKeyDict) != 0 return g } @@ -81,6 +123,23 @@ func (g *Guard) Secure(f func(w http.ResponseWriter, r *http.Request)) func(w ht } } +/* +Please refer to http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html for Rest Authentication details +*/ +func (g *Guard) CheckAuthorization(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + if !g.isActive { + //if no security needed, just skip all checkings + return f + } + return func(w http.ResponseWriter, r *http.Request) { + if err := g.checkAuthorization(w, r); err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + f(w, r) + } +} + func GetActualRemoteHost(r *http.Request) (host string, err error) { host = r.Header.Get("HTTP_X_FORWARDED_FOR") if host == "" { @@ -160,3 +219,194 @@ func (g *Guard) checkJwt(w http.ResponseWriter, r *http.Request) error { glog.V(1).Infof("No permission from %s", r.RemoteAddr) return fmt.Errorf("No write permisson from %s", r.RemoteAddr) } + +func ComputeHmac256(message string, secret string) string { + key := []byte(secret) + h := hmac.New(sha256.New, key) + h.Write([]byte(message)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +/* +authentication sample: +Authorization: AWS-HMAC-SHA256 PLLZOBTTZXGBNOWUFHZZ:tuXu/KcggHWPAfEmraUHDwEUdiIPSXVRsO+T2rxomBQ= +*/ +func VerifyAuthorizationHeader(authorization string) (accessKeyId, signature string, err error) { + authArr := strings.Split(authorization, " ") + if len(authArr) != 2 { + err = fmt.Errorf("authorization %s is not correct", authorization) + return + } + + /* + This is just a sample, you could customize the signature algorithm freely + */ + if authArr[0] != "AWS-HMAC-SHA256" { + err = fmt.Errorf("algorithm %s is not supported yet", authArr[0]) + return + } + + sigArr := strings.Split(authArr[1], ":") + if len(sigArr) != 2 { + err = fmt.Errorf("signature %s is not correct", authArr[1]) + return + } + accessKeyId = sigArr[0] + signature = sigArr[1] + if len(accessKeyId) == 0 || len(signature) == 0 { + err = fmt.Errorf("accessKeyId %s or signature %s is empty", accessKeyId, signature) + return + } + return + +} + +func (g *Guard) GetAccessKeyAppContent(accessKeyId string) (AppContent, error) { + + if appContent, found := g.appKeyDict[accessKeyId]; !found { + return AppContent{}, fmt.Errorf("%s key Not Found", accessKeyId) + } else { + return appContent, nil + } +} + +func GetBucketAndObjectName(r *http.Request) (bucketName, objectName string) { + /* + http://host:port/objectName?collection=bucketName + http://host:port/bucketName/objectName + */ + + collection := r.URL.Query().Get("collection") + if collection != "" { + return collection, r.URL.Path[1:] + } + + secondPos := strings.Index(r.URL.Path[1:], "/") + if secondPos == -1 { + secondPos = len(r.URL.Path) + } else { + secondPos += 1 + } + bucketName = r.URL.Path[1:secondPos] + if secondPos+1 < len(r.URL.Path) { + objectName = r.URL.Path[secondPos+1:] + } + return +} + +type headerSorter struct { + Keys []string + Vals []string +} + +func newHeaderSorter(m map[string]string) *headerSorter { + hs := &headerSorter{ + Keys: make([]string, 0, len(m)), + Vals: make([]string, 0, len(m)), + } + + for k, v := range m { + hs.Keys = append(hs.Keys, k) + hs.Vals = append(hs.Vals, v) + } + return hs +} + +func (hs *headerSorter) Sort() { + sort.Sort(hs) +} + +func (hs *headerSorter) Len() int { + return len(hs.Vals) +} + +func (hs *headerSorter) Less(i, j int) bool { + return bytes.Compare([]byte(hs.Keys[i]), []byte(hs.Keys[j])) < 0 +} + +func (hs *headerSorter) Swap(i, j int) { + hs.Vals[i], hs.Vals[j] = hs.Vals[j], hs.Vals[i] + hs.Keys[i], hs.Keys[j] = hs.Keys[j], hs.Keys[i] +} + +func (g *Guard) GenerateSignature(appKeySecret string, r *http.Request) (string, error) { + unescaped_uri, err := url.QueryUnescape(r.RequestURI) + if err != nil { + glog.V(1).Infof("uri %s unescape err %s", unescaped_uri, err.Error()) + return "", ErrUnauthorized + } + + temp := make(map[string]string) + /* + if there are some header options which were not prepared to added into the canonicalizedOSSHeaders, + add a prefix as below and bypass + */ + for k, v := range r.Header { + if !strings.HasPrefix(strings.ToLower(k), "x-aws-") { + temp[strings.ToLower(k)] = v[0] + } + } + hs := newHeaderSorter(temp) + hs.Sort() + + // Get the CanonicalizedOSSHeaders + canonicalizedOSSHeaders := "" + for i := range hs.Keys { + canonicalizedOSSHeaders += hs.Keys[i] + ":" + hs.Vals[i] + "\n" + } + + string_to_sign := r.Method + "\n" + + r.Header.Get("Content-MD5") + "\n" + + r.Header.Get("Content-Type") + "\n" + + r.Header.Get("Date") + "\n" + + canonicalizedOSSHeaders + + unescaped_uri + + return ComputeHmac256(string_to_sign, appKeySecret), nil +} + +func (g *Guard) checkAuthorization(w http.ResponseWriter, r *http.Request) error { + authorization_string_from_client := r.Header.Get("Authorization") + if len(authorization_string_from_client) == 0 { + glog.V(1).Infof("Authorization in header field is empty") + return ErrUnauthorized + } + + accessKeyId, signature, err := VerifyAuthorizationHeader(authorization_string_from_client) + if err != nil { + glog.V(1).Infof("verify authorization failed %s", err.Error()) + return ErrUnauthorized + } + + appContent, err := g.GetAccessKeyAppContent(accessKeyId) + if err != nil { + glog.V(1).Infof("accessKeyId %s is invalid", accessKeyId) + return ErrUnauthorized + } + + bucketName, _ := GetBucketAndObjectName(r) + + var found bool + for _, bucket := range appContent.BucketList { + if bucket == bucketName { + found = true + break + } + } + if !found { + glog.V(1).Infof("bucketName %s is not authorized for appKeyId %s", bucketName, accessKeyId) + return ErrUnauthorized + } + if my_sig, err := g.GenerateSignature(appContent.AppKeySecret, r); err != nil { + return err + } else { + if signature != my_sig { + glog.V(1).Infof("Authorization[%s]'s signature from client is not valid", authorization_string_from_client) + return ErrUnauthorized + } else { + glog.V(3).Infof("Authorization succeeded!") + } + } + + return nil +} diff --git a/weed/server/filer_server.go b/weed/server/filer_server.go index 4a0b2103b..ce0aa791d 100644 --- a/weed/server/filer_server.go +++ b/weed/server/filer_server.go @@ -26,6 +26,7 @@ type filerConf struct { MysqlConf []mysql_store.MySqlConf `json:"mysql"` mysql_store.ShardingConf PostgresConf *postgres_store.PostgresConf `json:"postgres"` + AppConf []security.AppConf `json:"appInfo"` } func parseConfFile(confPath string) (*filerConf, error) { @@ -55,6 +56,7 @@ type FilerServer struct { filer filer.Filer maxMB int masterNodes *storage.MasterNodes + guard *security.Guard } func NewFilerServer(r *http.ServeMux, ip string, port int, master string, dir string, collection string, @@ -109,6 +111,11 @@ func NewFilerServer(r *http.ServeMux, ip string, port int, master string, dir st r.HandleFunc("/admin/register", fs.registerHandler) } + /* + guard := security.NewGuard(nil, secret, setting.AppConf) + fs.guard = guard + r.HandleFunc("/", guard.CheckAuthorization(fs.filerHandler)) + */ r.HandleFunc("/", fs.filerHandler) go func() { diff --git a/weed/server/master_server.go b/weed/server/master_server.go index 61bda6988..c9648f1ef 100644 --- a/weed/server/master_server.go +++ b/weed/server/master_server.go @@ -58,7 +58,7 @@ func NewMasterServer(r *mux.Router, port int, metaFolder string, ms.vg = topology.NewDefaultVolumeGrowth() glog.V(0).Infoln("Volume Size Limit is", volumeSizeLimitMB, "MB") - ms.guard = security.NewGuard(whiteList, secureKey) + ms.guard = security.NewGuard(whiteList, secureKey, nil) r.HandleFunc("/", ms.uiStatusHandler) r.HandleFunc("/ui/index.html", ms.uiStatusHandler) diff --git a/weed/server/volume_server.go b/weed/server/volume_server.go index 1a912a169..81b8f4870 100644 --- a/weed/server/volume_server.go +++ b/weed/server/volume_server.go @@ -47,7 +47,7 @@ func NewVolumeServer(adminMux, publicMux *http.ServeMux, ip string, vs.store = storage.NewStore(port, ip, publicUrl, folders, maxCounts, vs.needleMapKind) storage.EnableBytesCache = enableBytesCache - vs.guard = security.NewGuard(whiteList, "") + vs.guard = security.NewGuard(whiteList, "", nil) adminMux.HandleFunc("/ui/index.html", vs.uiStatusHandler) adminMux.HandleFunc("/status", vs.guard.WhiteList(vs.statusHandler))