diff --git a/src/client/http.go b/src/client/files.go similarity index 64% rename from src/client/http.go rename to src/client/files.go index 83f6b4b..8c996ac 100644 --- a/src/client/http.go +++ b/src/client/files.go @@ -8,24 +8,24 @@ import ( "github.com/parnurzeal/gorequest" ) -type QSClient struct { +type FilesClient struct { addr string r *gorequest.SuperAgent } -func NewQSClient(addr string) *QSClient { +func NewFilesClient(addr string) *FilesClient { gr := gorequest.New() - return &QSClient{ + return &FilesClient{ addr: addr, r: gr, } } -func (cl *QSClient) url(urlpath string) string { +func (cl *FilesClient) url(urlpath string) string { return fmt.Sprintf("%s%s", cl.addr, urlpath) } -func (cl *QSClient) Create(filepath string, size int64) (gorequest.Response, string, []error) { +func (cl *FilesClient) Create(filepath string, size int64) (gorequest.Response, string, []error) { return cl.r.Post(cl.url("/v1/fs/files")). Send(fileshdr.CreateReq{ Path: filepath, @@ -34,13 +34,13 @@ func (cl *QSClient) Create(filepath string, size int64) (gorequest.Response, str End() } -func (cl *QSClient) Delete(filepath string) (gorequest.Response, string, []error) { +func (cl *FilesClient) Delete(filepath string) (gorequest.Response, string, []error) { return cl.r.Delete(cl.url("/v1/fs/files")). Param(fileshdr.FilePathQuery, filepath). End() } -func (cl *QSClient) Metadata(filepath string) (gorequest.Response, *fileshdr.MetadataResp, []error) { +func (cl *FilesClient) Metadata(filepath string) (gorequest.Response, *fileshdr.MetadataResp, []error) { resp, body, errs := cl.r.Get(cl.url("/v1/fs/metadata")). Param(fileshdr.FilePathQuery, filepath). End() @@ -54,13 +54,13 @@ func (cl *QSClient) Metadata(filepath string) (gorequest.Response, *fileshdr.Met return resp, mResp, nil } -func (cl *QSClient) Mkdir(dirpath string) (gorequest.Response, string, []error) { +func (cl *FilesClient) Mkdir(dirpath string) (gorequest.Response, string, []error) { return cl.r.Post(cl.url("/v1/fs/dirs")). Send(fileshdr.MkdirReq{Path: dirpath}). End() } -func (cl *QSClient) Move(oldpath, newpath string) (gorequest.Response, string, []error) { +func (cl *FilesClient) Move(oldpath, newpath string) (gorequest.Response, string, []error) { return cl.r.Patch(cl.url("/v1/fs/files/move")). Send(fileshdr.MoveReq{ OldPath: oldpath, @@ -69,7 +69,7 @@ func (cl *QSClient) Move(oldpath, newpath string) (gorequest.Response, string, [ End() } -func (cl *QSClient) UploadChunk(filepath string, content string, offset int64) (gorequest.Response, string, []error) { +func (cl *FilesClient) UploadChunk(filepath string, content string, offset int64) (gorequest.Response, string, []error) { return cl.r.Patch(cl.url("/v1/fs/files/chunks")). Send(fileshdr.UploadChunkReq{ Path: filepath, @@ -79,7 +79,7 @@ func (cl *QSClient) UploadChunk(filepath string, content string, offset int64) ( End() } -func (cl *QSClient) UploadStatus(filepath string) (gorequest.Response, *fileshdr.UploadStatusResp, []error) { +func (cl *FilesClient) UploadStatus(filepath string) (gorequest.Response, *fileshdr.UploadStatusResp, []error) { resp, body, errs := cl.r.Get(cl.url("/v1/fs/files/chunks")). Param(fileshdr.FilePathQuery, filepath). End() @@ -93,7 +93,7 @@ func (cl *QSClient) UploadStatus(filepath string) (gorequest.Response, *fileshdr return resp, uResp, nil } -func (cl *QSClient) Download(filepath string, headers map[string]string) (gorequest.Response, string, []error) { +func (cl *FilesClient) Download(filepath string, headers map[string]string) (gorequest.Response, string, []error) { r := cl.r.Get(cl.url("/v1/fs/files/chunks")). Param(fileshdr.FilePathQuery, filepath) for key, val := range headers { @@ -102,7 +102,7 @@ func (cl *QSClient) Download(filepath string, headers map[string]string) (gorequ return r.End() } -func (cl *QSClient) List(dirPath string) (gorequest.Response, *fileshdr.ListResp, []error) { +func (cl *FilesClient) List(dirPath string) (gorequest.Response, *fileshdr.ListResp, []error) { resp, body, errs := cl.r.Get(cl.url("/v1/fs/dirs")). Param(fileshdr.ListDirQuery, dirPath). End() diff --git a/src/client/singleuser.go b/src/client/singleuser.go new file mode 100644 index 0000000..976adb0 --- /dev/null +++ b/src/client/singleuser.go @@ -0,0 +1,55 @@ +package client + +import ( + "fmt" + "net/http" + + su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr" + "github.com/parnurzeal/gorequest" +) + +type SingleUserClient struct { + addr string + r *gorequest.SuperAgent +} + +func NewSingleUserClient(addr string) *SingleUserClient { + gr := gorequest.New() + return &SingleUserClient{ + addr: addr, + r: gr, + } +} + +func (cl *SingleUserClient) url(urlpath string) string { + return fmt.Sprintf("%s%s", cl.addr, urlpath) +} + +func (cl *SingleUserClient) Login(user, pwd string) (*http.Response, string, []error) { + return cl.r.Post(cl.url("/v1/users/login")). + Send(su.LoginReq{ + User: user, + Pwd: pwd, + }). + End() +} + +func (cl *SingleUserClient) Logout(user string, token *http.Cookie) (*http.Response, string, []error) { + return cl.r.Post(cl.url("/v1/users/logout")). + Send(su.LogoutReq{ + User: user, + }). + AddCookie(token). + End() +} + +func (cl *SingleUserClient) SetPwd(user, oldPwd, newPwd string, token *http.Cookie) (*http.Response, string, []error) { + return cl.r.Patch(cl.url("/v1/users/pwd")). + Send(su.SetPwdReq{ + User: user, + OldPwd: oldPwd, + NewPwd: newPwd, + }). + AddCookie(token). + End() +} diff --git a/src/client/utils.go b/src/client/utils.go new file mode 100644 index 0000000..50b75ec --- /dev/null +++ b/src/client/utils.go @@ -0,0 +1,12 @@ +package client + +import "net/http" + +func GetCookie(cookies []*http.Cookie, name string) *http.Cookie { + for _, c := range cookies { + if c.Name == name { + return c + } + } + return nil +} diff --git a/src/fs/local/fs.go b/src/fs/local/fs.go index e9225be..0e76ef4 100644 --- a/src/fs/local/fs.go +++ b/src/fs/local/fs.go @@ -37,6 +37,7 @@ func NewLocalFS(root string, defaultPerm uint32, opensLimit, openTTL int) *Local if root == "" { root = "." } + return &LocalFS{ root: root, defaultPerm: os.FileMode(defaultPerm), diff --git a/src/handlers/singleuserhdr/handlers.go b/src/handlers/singleuserhdr/handlers.go index 86d4b70..dc5f122 100644 --- a/src/handlers/singleuserhdr/handlers.go +++ b/src/handlers/singleuserhdr/handlers.go @@ -71,22 +71,29 @@ func generatePwd() (string, error) { return fmt.Sprintf("%x", sha1.Sum(buf[:size]))[:8], nil } -func (h *SimpleUserHandlers) Init(userName string) (string, error) { +func (h *SimpleUserHandlers) Init(userName, pwd string) (string, error) { if userName == "" { return "", errors.New("user name can not be empty") } var err error - tmpPwd, err := generatePwd() + if pwd == "" { + tmpPwd, err := generatePwd() + if err != nil { + return "", err + } + pwd = tmpPwd + } + pwdHash, err := bcrypt.GenerateFromPassword([]byte(pwd), 10) if err != nil { return "", err } - err = h.deps.KV().SetStringIn(UsersNs, userName, tmpPwd) + err = h.deps.KV().SetStringIn(UsersNs, userName, string(pwdHash)) if err != nil { return "", err } - err = h.deps.KV().SetStringIn(RolesNs, RoleParam, AdminRole) + err = h.deps.KV().SetStringIn(RolesNs, userName, AdminRole) if err != nil { return "", err } @@ -95,37 +102,41 @@ func (h *SimpleUserHandlers) Init(userName string) (string, error) { return "", err } - return tmpPwd, nil + return pwd, nil +} + +type LoginReq struct { + User string `json:"user"` + Pwd string `json:"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, 401, ErrInvalidUser)) + req := &LoginReq{} + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(q.ErrResp(c, 500, err)) return } - expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, user) + expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, req.User) if !ok { c.JSON(q.ErrResp(c, 500, ErrInvalidConfig)) return } - err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(pwd)) + err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.Pwd)) if err != nil { - c.JSON(q.ErrResp(c, 401, ErrInvalidUser)) + c.JSON(q.ErrResp(c, 401, err)) return } - role, ok := h.deps.KV().GetStringIn(RolesNs, user) + role, ok := h.deps.KV().GetStringIn(RolesNs, req.User) if !ok { - c.JSON(q.ErrResp(c, 500, ErrInvalidConfig)) + c.JSON(q.ErrResp(c, 501, ErrInvalidConfig)) return } ttl := h.cfg.GrabInt("Users.CookieTTL") token, err := h.deps.Token().ToToken(map[string]string{ - UserParam: user, + UserParam: req.User, RoleParam: role, ExpireParam: fmt.Sprintf("%d", time.Now().Unix()+int64(ttl)), }) @@ -142,39 +153,53 @@ func (h *SimpleUserHandlers) Login(c *gin.Context) { c.JSON(q.Resp(200)) } +type LogoutReq struct { + User string `json:"user"` +} + func (h *SimpleUserHandlers) Logout(c *gin.Context) { + req := &LogoutReq{} + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + // token alreay verified in the authn middleware c.SetCookie(TokenCookie, "", 0, "/", "nohost", false, true) c.JSON(q.Resp(200)) } +type SetPwdReq struct { + User string `json:"user"` + OldPwd string `json:"oldPwd"` + NewPwd string `json:"newPwd"` +} + func (h *SimpleUserHandlers) SetPwd(c *gin.Context) { - user, ok1 := c.GetPostForm(UserParam) - pwd1, ok2 := c.GetPostForm(PwdParam) - pwd2, ok3 := c.GetPostForm(NewPwdParam) - if !ok1 || !ok2 || !ok3 { - c.JSON(q.ErrResp(c, 401, ErrInvalidUser)) + req := &SetPwdReq{} + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(q.ErrResp(c, 400, err)) return } - expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, user) + expectedHash, ok := h.deps.KV().GetStringIn(UsersNs, req.User) if !ok { c.JSON(q.ErrResp(c, 500, ErrInvalidConfig)) return } - err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(pwd1)) + err := bcrypt.CompareHashAndPassword([]byte(expectedHash), []byte(req.OldPwd)) if err != nil { c.JSON(q.ErrResp(c, 401, ErrInvalidUser)) return } - newHash, err := bcrypt.GenerateFromPassword([]byte(pwd2), 10) + newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPwd), 10) if err != nil { c.JSON(q.ErrResp(c, 500, errors.New("fail to set password"))) return } - err = h.deps.KV().SetStringIn(UsersNs, user, string(newHash)) + err = h.deps.KV().SetStringIn(UsersNs, req.User, string(newHash)) if err != nil { c.JSON(q.ErrResp(c, 500, ErrInvalidConfig)) return diff --git a/src/handlers/singleuserhdr/middlewares.go b/src/handlers/singleuserhdr/middlewares.go index d1b8ac2..b6a2582 100644 --- a/src/handlers/singleuserhdr/middlewares.go +++ b/src/handlers/singleuserhdr/middlewares.go @@ -40,6 +40,7 @@ func (h *SimpleUserHandlers) Auth() gin.HandlerFunc { RoleParam: "", ExpireParam: "", } + _, err = h.deps.Token().FromToken(token, claims) if err != nil { c.JSON(q.ErrResp(c, 401, err)) @@ -54,8 +55,8 @@ func (h *SimpleUserHandlers) Auth() gin.HandlerFunc { } // visitor is only allowed to download - if claims[UserParam] != AdminRole && handlerName != "Download-fm" { - c.JSON(q.ErrResp(c, 401, err)) + if claims[RoleParam] != AdminRole && handlerName != "Download-fm" { + c.JSON(q.Resp(401)) return } } diff --git a/src/server/config.go b/src/server/config.go index 692d738..8eafbe3 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -9,11 +9,12 @@ type FSConfig struct { } type UsersCfg struct { - EnableAuth bool `json:"enableAuth"` - DefaultAdmin string `json:"defaultAdmin" cfg:"env"` - CookieTTL int `json:"cookieTTL"` - CookieSecure bool `json:"cookieSecure"` - CookieHttpOnly bool `json:"cookieHttpOnly"` + EnableAuth bool `json:"enableAuth"` + DefaultAdmin string `json:"defaultAdmin" cfg:"env"` + DefaultAdminPwd string `json:"defaultAdminPwd" cfg:"env"` + CookieTTL int `json:"cookieTTL"` + CookieSecure bool `json:"cookieSecure"` + CookieHttpOnly bool `json:"cookieHttpOnly"` } type Secrets struct { @@ -48,11 +49,12 @@ func DefaultConfig() (string, error) { OpenTTL: 60, // 1 min }, Users: &UsersCfg{ - EnableAuth: true, - DefaultAdmin: "", - CookieTTL: 3600 * 24 * 7, // 1 week - CookieSecure: false, - CookieHttpOnly: true, + EnableAuth: true, + DefaultAdmin: "", + DefaultAdminPwd: "", + CookieTTL: 3600 * 24 * 7, // 1 week + CookieSecure: false, + CookieHttpOnly: true, }, Secrets: &Secrets{ TokenSecret: "", diff --git a/src/server/server.go b/src/server/server.go index 90537b7..fc265da 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -70,7 +70,7 @@ func initDeps(cfg gocfg.ICfg) *depidx.Deps { filesystem := local.NewLocalFS(rootPath, 0660, opensLimit, openTTL) jwtEncDec := jwt.NewJWTEncDec(secret) logger := simplelog.NewSimpleLogger() - kv := boltdbpvd.New(".", 1024) + kv := boltdbpvd.New(rootPath, 1024) deps := depidx.NewDeps(cfg) deps.SetFS(filesystem) @@ -101,11 +101,13 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E fmt.Scanf("%s", &adminName) } - adminTmpPwd, err := userHdrs.Init(adminName) + adminPwd, _ := cfg.String("ENV.DEFAULTADMINPWD") + adminPwd, err := userHdrs.Init(adminName, adminPwd) if err != nil { return nil, err } - fmt.Printf("%s is created, its password is %s, please update it after login\n", adminName, adminTmpPwd) + + fmt.Printf("%s is created, its password is %s, please update it after login\n", adminName, adminPwd) } fileHdrs, err := fileshdr.NewFileHandlers(cfg, deps) @@ -122,6 +124,7 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E users := v1.Group("/users") users.POST("/login", userHdrs.Login) users.POST("/logout", userHdrs.Logout) + users.PATCH("/pwd", userHdrs.SetPwd) filesSvc := v1.Group("/fs") filesSvc.POST("/files", fileHdrs.Create) diff --git a/src/server/server_files_test.go b/src/server/server_files_test.go index 3c48d73..e27540f 100644 --- a/src/server/server_files_test.go +++ b/src/server/server_files_test.go @@ -9,39 +9,13 @@ import ( "testing" "time" - "github.com/ihexxa/gocfg" - "github.com/ihexxa/quickshare/src/client" "github.com/ihexxa/quickshare/src/handlers/fileshdr" ) -func startTestServer(config string) *Server { - defaultCfg, err := DefaultConfig() - if err != nil { - panic(err) - } - - cfg, err := gocfg.New(NewConfig()). - Load( - gocfg.JSONStr(defaultCfg), - gocfg.JSONStr(config), - ) - if err != nil { - panic(err) - } - - srv, err := NewServer(cfg) - if err != nil { - panic(err) - } - - go srv.Start() - return srv -} - func TestFileHandlers(t *testing.T) { addr := "http://127.0.0.1:8888" - root := "./testData" + root := "testData" chunkSize := 2 config := `{ "users": { @@ -51,16 +25,21 @@ func TestFileHandlers(t *testing.T) { "debug": true }, "fs": { - "root": "./testData" + "root": "testData" } }` + err := os.MkdirAll(root, 0700) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + srv := startTestServer(config) defer srv.Shutdown() // kv := srv.depsKVStore() fs := srv.depsFS() - defer os.RemoveAll(root) - cl := client.NewQSClient(addr) + cl := client.NewFilesClient(addr) // TODO: remove this time.Sleep(500) diff --git a/src/server/server_singleuser_test.go b/src/server/server_singleuser_test.go new file mode 100644 index 0000000..29a3a25 --- /dev/null +++ b/src/server/server_singleuser_test.go @@ -0,0 +1,78 @@ +package server + +import ( + "os" + "testing" + "time" + + "github.com/ihexxa/quickshare/src/client" + su "github.com/ihexxa/quickshare/src/handlers/singleuserhdr" +) + +func TestSingleUserHandlers(t *testing.T) { + addr := "http://127.0.0.1:8888" + root := "testData" + config := `{ + "users": { + "enableAuth": true + }, + "server": { + "debug": true + }, + "fs": { + "root": "testData" + } + }` + adminName := "qs" + adminPwd := "quicksh@re" + adminNewPwd := "quicksh@re2" + os.Setenv("DEFAULTADMIN", adminName) + os.Setenv("DEFAULTADMINPWD", adminPwd) + + err := os.MkdirAll(root, 0700) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(root) + + srv := startTestServer(config) + defer srv.Shutdown() + + suCl := client.NewSingleUserClient(addr) + // fCl := client.NewFilesClient(addr) + + // TODO: remove this + time.Sleep(1000) + + t.Run("test single user APIs: Login-SetPwd-Logout-Login", func(t *testing.T) { + resp, _, errs := suCl.Login(adminName, adminPwd) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + token := client.GetCookie(resp.Cookies(), su.TokenCookie) + + resp, _, errs = suCl.SetPwd(adminName, adminPwd, adminNewPwd, token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + resp, _, errs = suCl.Logout(adminName, token) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + + resp, _, errs = suCl.Login(adminName, adminNewPwd) + if len(errs) > 0 { + t.Fatal(errs) + } else if resp.StatusCode != 200 { + t.Fatal(resp.StatusCode) + } + }) +} diff --git a/src/server/test_helpers.go b/src/server/test_helpers.go new file mode 100644 index 0000000..6523f9f --- /dev/null +++ b/src/server/test_helpers.go @@ -0,0 +1,27 @@ +package server + +import "github.com/ihexxa/gocfg" + +func startTestServer(config string) *Server { + defaultCfg, err := DefaultConfig() + if err != nil { + panic(err) + } + + cfg, err := gocfg.New(NewConfig()). + Load( + gocfg.JSONStr(defaultCfg), + gocfg.JSONStr(config), + ) + if err != nil { + panic(err) + } + + srv, err := NewServer(cfg) + if err != nil { + panic(err) + } + + go srv.Start() + return srv +}