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

2
go.mod
View file

@ -12,6 +12,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/robbert229/jwt v2.0.0+incompatible github.com/robbert229/jwt v2.0.0+incompatible
github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c 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 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb // indirect
moul.io/http2curl v1.0.0 // 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/gocfg => /home/hexxa/ws/github.com/ihexxa/gocfg
replace github.com/ihexxa/multipart => /home/hexxa/ws/github.com/ihexxa/multipart replace github.com/ihexxa/multipart => /home/hexxa/ws/github.com/ihexxa/multipart

1
go.sum
View file

@ -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= 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-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-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/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/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= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=

View file

@ -2,15 +2,30 @@ package singleuserhdr
import ( import (
"errors" "errors"
"fmt"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/ihexxa/gocfg" "github.com/ihexxa/gocfg"
"golang.org/x/crypto/bcrypt"
"github.com/ihexxa/quickshare/src/depidx" "github.com/ihexxa/quickshare/src/depidx"
q "github.com/ihexxa/quickshare/src/handlers" 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 { type SimpleUserHandlers struct {
cfg gocfg.ICfg cfg gocfg.ICfg
@ -24,52 +39,52 @@ func NewSimpleUserHandlers(cfg gocfg.ICfg, deps *depidx.Deps) *SimpleUserHandler
} }
} }
func (hdr *SimpleUserHandlers) Login(c *gin.Context) { func (h *SimpleUserHandlers) Login(c *gin.Context) {
userName := c.Query("username") user, ok1 := c.GetPostForm(UserParam)
pwd := c.Query("pwd") pwd, ok2 := c.GetPostForm(PwdParam)
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")
if !ok1 || !ok2 { if !ok1 || !ok2 {
c.JSON(q.ErrResp(c, 400, ErrInvalidUser)) c.JSON(q.ErrResp(c, 401, ErrInvalidUser))
return return
} }
if userName != expectedName || pwd != expectedPwd { expectedHash, ok := h.deps.KV().GetStringIn(UsersNamespace, user)
c.JSON(q.ErrResp(c, 400, ErrInvalidUser)) if !ok {
c.JSON(q.ErrResp(c, 500, ErrInvalidConfig))
return 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 { if err != nil {
c.JSON(q.ErrResp(c, 500, err)) c.JSON(q.ErrResp(c, 500, err))
return return
} }
// TODO: use config hostname := h.cfg.GrabString("Server.Host")
c.SetCookie("token", token, 3600, "/", "localhost", false, true) 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)) c.JSON(q.Resp(200))
} }
func (hdr *SimpleUserHandlers) Logout(c *gin.Context) { func (h *SimpleUserHandlers) Logout(c *gin.Context) {
token, err := c.Cookie("token") // token alreay verified in the authn middleware
if err != nil { c.SetCookie(TokenCookie, "", 0, "/", "nohost", false, true)
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)
c.JSON(q.Resp(200)) 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"} buckets := []string{"bools", "ints", "int64s", "floats", "strings", "locks"}
for _, bucketName := range buckets { for _, bucketName := range buckets {
// TODO: should return err
db.Update(func(tx *bolt.Tx) error { db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucketName)) b := tx.Bucket([]byte(bucketName))
if b != nil { 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 { func (bp *BoltPvd) Close() error {
return bp.db.Close() return bp.db.Close()
} }
@ -223,3 +236,26 @@ func (bp *BoltPvd) Unlock(key string) error {
return kvstore.ErrNoLock 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") var ErrNoLock = errors.New("no lock to unlock")
type IKVStore interface { type IKVStore interface {
AddNamespace(nsName string) error
GetBool(key string) (bool, bool) GetBool(key string) (bool, bool)
SetBool(key string, val bool) error SetBool(key string, val bool) error
DelBool(key string) error DelBool(key string) error
@ -19,8 +20,10 @@ type IKVStore interface {
SetFloat(key string, val float64) error SetFloat(key string, val float64) error
DelFloat(key string) error DelFloat(key string) error
GetString(key string) (string, bool) GetString(key string) (string, bool)
SetString(key string, val string) error SetString(key, val string) error
DelString(key string) error DelString(key string) error
GetStringIn(namespace, key string) (string, bool)
SetStringIn(namespace, key, val string) error
TryLock(key string) error TryLock(key string) error
Unlock(key string) error Unlock(key string) error
} }

View file

@ -6,13 +6,21 @@ type FSConfig struct {
OpenTTL int `json:"openTTL"` 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 { type Secrets struct {
TokenSecret string `json:"tokenSecret" cfg:"env"` TokenSecret string `json:"tokenSecret" cfg:"env"`
} }
type ServerCfg struct { type ServerCfg struct {
Debug bool `json:"debug"` Debug bool `json:"debug"`
Addr string `json:"addr"` Host string `json:"host"`
Port int `json:"port"`
ReadTimeout int `json:"readTimeout"` ReadTimeout int `json:"readTimeout"`
WriteTimeout int `json:"writeTimeout"` WriteTimeout int `json:"writeTimeout"`
MaxHeaderBytes int `json:"maxHeaderBytes"` MaxHeaderBytes int `json:"maxHeaderBytes"`
@ -22,6 +30,7 @@ type Config struct {
Fs *FSConfig `json:"fs"` Fs *FSConfig `json:"fs"`
Secrets *Secrets `json:"secrets"` Secrets *Secrets `json:"secrets"`
Server *ServerCfg `json:"server"` Server *ServerCfg `json:"server"`
Users *UsersCfg `json:"users"`
} }
func NewEmptyConfig() *Config { func NewEmptyConfig() *Config {
@ -35,12 +44,19 @@ func NewDefaultConfig() *Config {
OpensLimit: 128, OpensLimit: 128,
OpenTTL: 60, // 1 min OpenTTL: 60, // 1 min
}, },
Users: &UsersCfg{
EnableAuth: true,
CookieTTL: 3600 * 24 * 7, // 1 week
CookieSecure: false,
CookieHttpOnly: true,
},
Secrets: &Secrets{ Secrets: &Secrets{
TokenSecret: "", TokenSecret: "",
}, },
Server: &ServerCfg{ Server: &ServerCfg{
Debug: false, Debug: false,
Addr: "127.0.0.1:8888", Host: "127.0.0.1",
Port: 8888,
ReadTimeout: 2000, ReadTimeout: 2000,
WriteTimeout: 2000, WriteTimeout: 2000,
MaxHeaderBytes: 512, MaxHeaderBytes: 512,

Binary file not shown.

View file

@ -43,7 +43,7 @@ func NewServer(cfg gocfg.ICfg) (*Server, error) {
srv := &http.Server{ srv := &http.Server{
// TODO: set more options // TODO: set more options
Addr: cfg.GrabString("Server.Addr"), Addr: fmt.Sprintf("%s:%d", cfg.GrabString("Server.Host"), cfg.GrabInt("Server.Port")),
Handler: router, Handler: router,
ReadTimeout: time.Duration(cfg.GrabInt("Server.ReadTimeout")) * time.Millisecond, ReadTimeout: time.Duration(cfg.GrabInt("Server.ReadTimeout")) * time.Millisecond,
WriteTimeout: time.Duration(cfg.GrabInt("Server.WriteTimeout")) * 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") opensLimit := cfg.GrabInt("Fs.OpensLimit")
openTTL := cfg.GrabInt("Fs.OpenTTL") openTTL := cfg.GrabInt("Fs.OpenTTL")
ider := simpleidgen.New()
filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL) filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL)
jwtEncDec := jwt.NewJWTEncDec(secret) jwtEncDec := jwt.NewJWTEncDec(secret)
logger := simplelog.NewSimpleLogger() logger := simplelog.NewSimpleLogger()
kv := boltdbpvd.New(".", 1024) 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 := depidx.NewDeps(cfg)
deps.SetFS(filesystem) 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) { 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") v1 := router.Group("/v1")
users := v1.Group("/users") users := v1.Group("/users")
userHdrs := singleuserhdr.NewSimpleUserHandlers(cfg, deps)
users.POST("/login", userHdrs.Login) users.POST("/login", userHdrs.Login)
users.POST("/logout", userHdrs.Logout) users.POST("/logout", userHdrs.Logout)
filesSvc := v1.Group("/fs") filesSvc := v1.Group("/fs")
fileHdrs, err := fileshdr.NewFileHandlers(cfg, deps)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View file

@ -36,11 +36,14 @@ func TestFileHandlers(t *testing.T) {
root := "./testData" root := "./testData"
chunkSize := 2 chunkSize := 2
config := `{ config := `{
"Server": { "users": {
"Debug": true "enableAuth": false
}, },
"FS": { "server": {
"Root": "./testData" "debug": true
},
"fs": {
"root": "./testData"
} }
}` }`