fix(singleuser): fix bugs in single user handlers

This commit is contained in:
hexxa 2020-12-05 22:00:20 +08:00
parent 31a1a331f7
commit 4d6e7ff938
10 changed files with 194 additions and 45 deletions

View file

@ -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))
}

View file

@ -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()
}
}

View file

@ -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))
})
}

View file

@ -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
}

View file

@ -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,

Binary file not shown.

View file

@ -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)
}

View file

@ -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"
}
}`