diff --git a/go.mod b/go.mod index d3fa310..c9428fa 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/robbert229/jwt v2.0.0+incompatible github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb // indirect moul.io/http2curl v1.0.0 // indirect ) @@ -19,4 +20,3 @@ require ( replace github.com/ihexxa/gocfg => /home/hexxa/ws/github.com/ihexxa/gocfg replace github.com/ihexxa/multipart => /home/hexxa/ws/github.com/ihexxa/multipart - diff --git a/go.sum b/go.sum index 2cb18f3..c405413 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,7 @@ go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= diff --git a/src/handlers/singleuserhdr/handlers.go b/src/handlers/singleuserhdr/handlers.go index efb54fa..058707d 100644 --- a/src/handlers/singleuserhdr/handlers.go +++ b/src/handlers/singleuserhdr/handlers.go @@ -2,15 +2,30 @@ package singleuserhdr import ( "errors" + "fmt" + "time" "github.com/gin-gonic/gin" "github.com/ihexxa/gocfg" + "golang.org/x/crypto/bcrypt" "github.com/ihexxa/quickshare/src/depidx" q "github.com/ihexxa/quickshare/src/handlers" ) -var ErrInvalidUser = errors.New("invalid user name or password") +var ( + ErrInvalidUser = errors.New("invalid user name or password") + ErrInvalidConfig = errors.New("invalid user config") + UserParam = "user" + PwdParam = "pwd" + RoleParam = "role" + ExpireParam = "expire" + TokenCookie = "tk" + AdminRole = "admin" + VisitorRole = "visitor" + UsersNamespace = "users" + RolesNamespace = "roles" +) type SimpleUserHandlers struct { cfg gocfg.ICfg @@ -24,52 +39,52 @@ func NewSimpleUserHandlers(cfg gocfg.ICfg, deps *depidx.Deps) *SimpleUserHandler } } -func (hdr *SimpleUserHandlers) Login(c *gin.Context) { - userName := c.Query("username") - pwd := c.Query("pwd") - if userName == "" || pwd == "" { - c.JSON(q.ErrResp(c, 400, ErrInvalidUser)) - return - } - - expectedName, ok1 := hdr.deps.KV().GetString("username") - expectedPwd, ok2 := hdr.deps.KV().GetString("pwd") +func (h *SimpleUserHandlers) Login(c *gin.Context) { + user, ok1 := c.GetPostForm(UserParam) + pwd, ok2 := c.GetPostForm(PwdParam) if !ok1 || !ok2 { - c.JSON(q.ErrResp(c, 400, ErrInvalidUser)) + c.JSON(q.ErrResp(c, 401, ErrInvalidUser)) return } - if userName != expectedName || pwd != expectedPwd { - c.JSON(q.ErrResp(c, 400, ErrInvalidUser)) + expectedHash, ok := h.deps.KV().GetStringIn(UsersNamespace, user) + if !ok { + c.JSON(q.ErrResp(c, 500, ErrInvalidConfig)) return } - token, err := hdr.deps.Token().ToToken(map[string]string{ - "username": expectedName, + + err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(pwd)) + if err != nil { + c.JSON(q.ErrResp(c, 401, ErrInvalidUser)) + return + } + + role, ok := h.deps.KV().GetStringIn(RolesNamespace, user) + if !ok { + c.JSON(q.ErrResp(c, 500, ErrInvalidConfig)) + return + } + ttl := h.cfg.GrabInt("Users.CookieTTL") + token, err := h.deps.Token().ToToken(map[string]string{ + UserParam: user, + RoleParam: role, + ExpireParam: fmt.Sprintf("%d", time.Now().Unix()+int64(ttl)), }) if err != nil { c.JSON(q.ErrResp(c, 500, err)) return } - // TODO: use config - c.SetCookie("token", token, 3600, "/", "localhost", false, true) + hostname := h.cfg.GrabString("Server.Host") + secure := h.cfg.GrabBool("Users.CookieSecure") + httpOnly := h.cfg.GrabBool("Users.CookieHttpOnly") + c.SetCookie(TokenCookie, token, ttl, "/", hostname, secure, httpOnly) + c.JSON(q.Resp(200)) } -func (hdr *SimpleUserHandlers) Logout(c *gin.Context) { - token, err := c.Cookie("token") - if err != nil { - c.JSON(q.ErrResp(c, 400, err)) - return - } - - // TODO: // check if token expired - _, err = hdr.deps.Token().FromToken(token, map[string]string{"token": ""}) - if err != nil { - c.JSON(q.ErrResp(c, 400, err)) - return - } - - c.SetCookie("token", "", 0, "/", "localhost", false, true) +func (h *SimpleUserHandlers) Logout(c *gin.Context) { + // token alreay verified in the authn middleware + c.SetCookie(TokenCookie, "", 0, "/", "nohost", false, true) c.JSON(q.Resp(200)) } diff --git a/src/handlers/singleuserhdr/middlewares.go b/src/handlers/singleuserhdr/middlewares.go new file mode 100644 index 0000000..d1b8ac2 --- /dev/null +++ b/src/handlers/singleuserhdr/middlewares.go @@ -0,0 +1,65 @@ +package singleuserhdr + +import ( + "errors" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + q "github.com/ihexxa/quickshare/src/handlers" +) + +func GetHandlerName(fullname string) (string, error) { + parts := strings.Split(fullname, ".") + if len(parts) == 0 { + return "", errors.New("invalid handler name") + } + return parts[len(parts)-1], nil +} + +func (h *SimpleUserHandlers) Auth() gin.HandlerFunc { + return func(c *gin.Context) { + handlerName, err := GetHandlerName(c.HandlerName()) + if err != nil { + c.JSON(q.ErrResp(c, 401, err)) + return + } + + // TODO: may also check the path + enableAuth := h.cfg.GrabBool("Users.EnableAuth") + if enableAuth && handlerName != "Login-fm" { + token, err := c.Cookie(TokenCookie) + if err != nil { + c.JSON(q.ErrResp(c, 401, err)) + return + } + + claims := map[string]string{ + UserParam: "", + RoleParam: "", + ExpireParam: "", + } + _, err = h.deps.Token().FromToken(token, claims) + if err != nil { + c.JSON(q.ErrResp(c, 401, err)) + return + } + + now := time.Now().Unix() + expire, err := strconv.ParseInt(claims[ExpireParam], 10, 64) + if err != nil || expire <= now { + c.JSON(q.ErrResp(c, 401, err)) + return + } + + // visitor is only allowed to download + if claims[UserParam] != AdminRole && handlerName != "Download-fm" { + c.JSON(q.ErrResp(c, 401, err)) + return + } + } + + c.Next() + } +} diff --git a/src/kvstore/boltdbpvd/provider.go b/src/kvstore/boltdbpvd/provider.go index dedd97f..da69b84 100644 --- a/src/kvstore/boltdbpvd/provider.go +++ b/src/kvstore/boltdbpvd/provider.go @@ -27,6 +27,7 @@ func New(dbPath string, maxStrLen int) *BoltPvd { buckets := []string{"bools", "ints", "int64s", "floats", "strings", "locks"} for _, bucketName := range buckets { + // TODO: should return err db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucketName)) if b != nil { @@ -48,6 +49,18 @@ func New(dbPath string, maxStrLen int) *BoltPvd { } } +func (bp *BoltPvd) AddNamespace(nsName string) error { + return bp.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(nsName)) + if b != nil { + return nil + } + + _, err := tx.CreateBucket([]byte(nsName)) + return err + }) +} + func (bp *BoltPvd) Close() error { return bp.db.Close() } @@ -223,3 +236,26 @@ func (bp *BoltPvd) Unlock(key string) error { return kvstore.ErrNoLock }) } + +func (bp *BoltPvd) GetStringIn(namespace, key string) (string, bool) { + buf, ok, length := make([]byte, bp.maxStrLen), false, bp.maxStrLen + bp.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(namespace)) + v := b.Get([]byte(key)) + length = copy(buf, v) + ok = v != nil + return nil + }) + return string(buf[:length]), ok +} + +func (bp *BoltPvd) SetStringIn(namespace, key, val string) error { + if len(val) > bp.maxStrLen { + return fmt.Errorf("can not set string value longer than %d", bp.maxStrLen) + } + + return bp.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(namespace)) + return b.Put([]byte(key), []byte(val)) + }) +} diff --git a/src/kvstore/kvstore_interface.go b/src/kvstore/kvstore_interface.go index cc99397..1bb4af4 100644 --- a/src/kvstore/kvstore_interface.go +++ b/src/kvstore/kvstore_interface.go @@ -6,6 +6,7 @@ var ErrLocked = errors.New("already locked") var ErrNoLock = errors.New("no lock to unlock") type IKVStore interface { + AddNamespace(nsName string) error GetBool(key string) (bool, bool) SetBool(key string, val bool) error DelBool(key string) error @@ -19,8 +20,10 @@ type IKVStore interface { SetFloat(key string, val float64) error DelFloat(key string) error GetString(key string) (string, bool) - SetString(key string, val string) error + SetString(key, val string) error DelString(key string) error + GetStringIn(namespace, key string) (string, bool) + SetStringIn(namespace, key, val string) error TryLock(key string) error Unlock(key string) error } diff --git a/src/server/config.go b/src/server/config.go index e0a179d..0fd9297 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -6,13 +6,21 @@ type FSConfig struct { OpenTTL int `json:"openTTL"` } +type UsersCfg struct { + EnableAuth bool `json:"enableAuth"` + CookieTTL int `json:"cookieTTL"` + CookieSecure bool `json:"cookieSecure"` + CookieHttpOnly bool `json:"cookieHttpOnly"` +} + type Secrets struct { TokenSecret string `json:"tokenSecret" cfg:"env"` } type ServerCfg struct { Debug bool `json:"debug"` - Addr string `json:"addr"` + Host string `json:"host"` + Port int `json:"port"` ReadTimeout int `json:"readTimeout"` WriteTimeout int `json:"writeTimeout"` MaxHeaderBytes int `json:"maxHeaderBytes"` @@ -22,6 +30,7 @@ type Config struct { Fs *FSConfig `json:"fs"` Secrets *Secrets `json:"secrets"` Server *ServerCfg `json:"server"` + Users *UsersCfg `json:"users"` } func NewEmptyConfig() *Config { @@ -35,12 +44,19 @@ func NewDefaultConfig() *Config { OpensLimit: 128, OpenTTL: 60, // 1 min }, + Users: &UsersCfg{ + EnableAuth: true, + CookieTTL: 3600 * 24 * 7, // 1 week + CookieSecure: false, + CookieHttpOnly: true, + }, Secrets: &Secrets{ TokenSecret: "", }, Server: &ServerCfg{ Debug: false, - Addr: "127.0.0.1:8888", + Host: "127.0.0.1", + Port: 8888, ReadTimeout: 2000, WriteTimeout: 2000, MaxHeaderBytes: 512, diff --git a/src/server/quickshare.db b/src/server/quickshare.db index ff17c5c..5555fa4 100644 Binary files a/src/server/quickshare.db and b/src/server/quickshare.db differ diff --git a/src/server/server.go b/src/server/server.go index 641794b..6baba22 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -43,7 +43,7 @@ func NewServer(cfg gocfg.ICfg) (*Server, error) { srv := &http.Server{ // TODO: set more options - Addr: cfg.GrabString("Server.Addr"), + Addr: fmt.Sprintf("%s:%d", cfg.GrabString("Server.Host"), cfg.GrabInt("Server.Port")), Handler: router, ReadTimeout: time.Duration(cfg.GrabInt("Server.ReadTimeout")) * time.Millisecond, WriteTimeout: time.Duration(cfg.GrabInt("Server.WriteTimeout")) * time.Millisecond, @@ -84,11 +84,17 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps { opensLimit := cfg.GrabInt("Fs.OpensLimit") openTTL := cfg.GrabInt("Fs.OpenTTL") + ider := simpleidgen.New() filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL) jwtEncDec := jwt.NewJWTEncDec(secret) logger := simplelog.NewSimpleLogger() kv := boltdbpvd.New(".", 1024) - ider := simpleidgen.New() + if err := kv.AddNamespace(singleuserhdr.UsersNamespace); err != nil { + panic(err) + } + if err := kv.AddNamespace(singleuserhdr.RolesNamespace); err != nil { + panic(err) + } deps := depidx.NewDeps(cfg) deps.SetFS(filesystem) @@ -107,15 +113,19 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps { } func addHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.Engine, error) { + userHdrs := singleuserhdr.NewSimpleUserHandlers(cfg, deps) + fileHdrs, err := fileshdr.NewFileHandlers(cfg, deps) + + // middleware + router.Use(userHdrs.Auth()) + v1 := router.Group("/v1") users := v1.Group("/users") - userHdrs := singleuserhdr.NewSimpleUserHandlers(cfg, deps) users.POST("/login", userHdrs.Login) users.POST("/logout", userHdrs.Logout) filesSvc := v1.Group("/fs") - fileHdrs, err := fileshdr.NewFileHandlers(cfg, deps) if err != nil { panic(err) } diff --git a/src/server/server_files_test.go b/src/server/server_files_test.go index 077d5b8..1a5094b 100644 --- a/src/server/server_files_test.go +++ b/src/server/server_files_test.go @@ -36,11 +36,14 @@ func TestFileHandlers(t *testing.T) { root := "./testData" chunkSize := 2 config := `{ - "Server": { - "Debug": true + "users": { + "enableAuth": false }, - "FS": { - "Root": "./testData" + "server": { + "debug": true + }, + "fs": { + "root": "./testData" } }`