diff --git a/assets/index.html b/assets/index.html index a0ad846d..c75d0c76 100644 --- a/assets/index.html +++ b/assets/index.html @@ -22,7 +22,7 @@ - <% for (var chunk of webpack.compilation.chunks) { + <% for (var chunk of webpack.chunks) { for (var file of chunk.files) { if (file.match(/\.(js|css)$/)) { %> <% }}} %> diff --git a/bolt/config.go b/bolt/config.go new file mode 100644 index 00000000..6dcd0378 --- /dev/null +++ b/bolt/config.go @@ -0,0 +1,17 @@ +package bolt + +import ( + "github.com/asdine/storm" +) + +type ConfigStore struct { + DB *storm.DB +} + +func (c ConfigStore) Get(name string, to interface{}) error { + return c.DB.Get("config", name, to) +} + +func (c ConfigStore) Save(name string, from interface{}) error { + return c.DB.Set("config", name, from) +} diff --git a/bolt/share.go b/bolt/share.go new file mode 100644 index 00000000..d66c08bb --- /dev/null +++ b/bolt/share.go @@ -0,0 +1,36 @@ +package bolt + +import ( + "github.com/asdine/storm" + fm "github.com/hacdias/filemanager" +) + +type ShareStore struct { + DB *storm.DB +} + +func (s ShareStore) Get(hash string) (*fm.ShareLink, error) { + var v *fm.ShareLink + err := s.DB.One("Hash", hash, &v) + return v, err +} + +func (s ShareStore) GetByPath(hash string) ([]*fm.ShareLink, error) { + var v []*fm.ShareLink + err := s.DB.Find("Path", hash, &v) + return v, err +} + +func (s ShareStore) Gets(hash string) ([]*fm.ShareLink, error) { + var v []*fm.ShareLink + err := s.DB.All(&v) + return v, err +} + +func (s ShareStore) Save(l *fm.ShareLink) error { + return s.DB.Save(l) +} + +func (s ShareStore) Delete(hash string) error { + return s.DB.DeleteStruct(&fm.ShareLink{Hash: hash}) +} diff --git a/bolt/users.go b/bolt/users.go new file mode 100644 index 00000000..880abfa1 --- /dev/null +++ b/bolt/users.go @@ -0,0 +1,55 @@ +package bolt + +import ( + "reflect" + + "github.com/asdine/storm" + fm "github.com/hacdias/filemanager" +) + +type UsersStore struct { + DB *storm.DB +} + +func (u UsersStore) Get(id int) (*fm.User, error) { + var us *fm.User + err := u.DB.One("ID", id, us) + if err == storm.ErrNotFound { + return nil, fm.ErrUserNotExist + } + + if err != nil { + return nil, err + } + + return &fm.User{}, nil +} + +func (u UsersStore) Gets() ([]*fm.User, error) { + var us []*fm.User + err := u.DB.All(us) + return us, err +} + +func (u UsersStore) Update(us *fm.User, fields ...string) error { + if len(fields) == 0 { + return u.Save(us) + } + + for _, field := range fields { + val := reflect.ValueOf(us).Elem().FieldByName(field).Interface() + if err := u.DB.UpdateField(us, field, val); err != nil { + return err + } + } + + return nil +} + +func (u UsersStore) Save(us *fm.User) error { + return u.DB.Save(us) +} + +func (u UsersStore) Delete(id int) error { + return u.DB.DeleteStruct(&fm.User{ID: id}) +} diff --git a/file.go b/file.go index 8beec058..367cec66 100644 --- a/file.go +++ b/file.go @@ -24,12 +24,12 @@ import ( ) var ( - errInvalidOption = errors.New("Invalid option") + ErrInvalidOption = errors.New("Invalid option") ) // File contains the information about a particular file or directory. type File struct { - // Indicates the Kind of view on the front-end (listing, editor or preview). + // Indicates the Kind of view on the front-end (Listing, editor or preview). Kind string `json:"kind"` // The name of the file. Name string `json:"name"` @@ -54,19 +54,19 @@ type File struct { // Stores the content of a text file. Content string `json:"content,omitempty"` - *listing `json:",omitempty"` + *Listing `json:",omitempty"` Metadata string `json:"metadata,omitempty"` Language string `json:"language,omitempty"` } -// A listing is the context used to fill out a template. -type listing struct { +// A Listing is the context used to fill out a template. +type Listing struct { // The items (files and folders) in the path. Items []*File `json:"items"` - // The number of directories in the listing. + // The number of directories in the Listing. NumDirs int `json:"numDirs"` - // The number of files (items that aren't directories) in the listing. + // The number of files (items that aren't directories) in the Listing. NumFiles int `json:"numFiles"` // Which sorting order is used. Sort string `json:"sort"` @@ -166,7 +166,7 @@ func (i *File) GetListing(u *User, r *http.Request) error { fileinfos = append(fileinfos, i) } - i.listing = &listing{ + i.Listing = &Listing{ Items: fileinfos, NumDirs: dirCount, NumFiles: fileCount, @@ -304,7 +304,7 @@ func (i File) Checksum(algo string) (string, error) { case "sha512": h = sha512.New() default: - return "", errInvalidOption + return "", ErrInvalidOption } _, err = io.Copy(h, file) @@ -321,7 +321,7 @@ func (i File) CanBeEdited() bool { } // ApplySort applies the sort order using .Order and .Sort -func (l listing) ApplySort() { +func (l Listing) ApplySort() { // Check '.Order' to know how to sort if l.Order == "desc" { switch l.Sort { @@ -350,10 +350,10 @@ func (l listing) ApplySort() { } } -// Implement sorting for listing -type byName listing -type bySize listing -type byModified listing +// Implement sorting for Listing +type byName Listing +type bySize Listing +type byModified Listing // By Name func (l byName) Len() int { diff --git a/filemanager.go b/filemanager.go index b2489745..928a6922 100644 --- a/filemanager.go +++ b/filemanager.go @@ -61,6 +61,7 @@ import ( "os/exec" "reflect" "strings" + "time" rice "github.com/GeertJohan/go.rice" "github.com/asdine/storm" @@ -71,17 +72,14 @@ import ( // FileManager is a file manager instance. It should be creating using the // 'New' function and not directly. type FileManager struct { - // The BoltDB database for this instance. - db *storm.DB + // Job cron. + Cron *cron.Cron // The key used to sign the JWT tokens. - key []byte + Key []byte // The static assets. - assets *rice.Box - - // Job cron. - cron *cron.Cron + Assets *rice.Box // PrefixURL is a part of the URL that is already trimmed from the request URL before it // arrives to our handlers. It may be useful when using File Manager as a middleware @@ -103,66 +101,42 @@ type FileManager struct { // The Default User needed to build the New User page. DefaultUser *User - // Users is a map with the different configurations for each user. - Users map[string]*User - // A map of events to a slice of commands. Commands map[string][]string Store *Store } -type Store struct { - Users *UsersStore -} - // Command is a command function. type Command func(r *http.Request, m *FileManager, u *User) error -/* - -// New creates a new File Manager instance. If 'database' file already -// exists, it will load the users from there. Otherwise, a new user -// will be created using the 'base' variable. The 'base' User should -// not have the Password field hashed. -func New(database string, base User) (*FileManager, error) { +func (m *FileManager) Load() error { // Creates a new File Manager instance with the Users // map and Assets box. - m := &FileManager{ - Users: map[string]*User{}, - cron: cron.New(), - assets: rice.MustFindBox("./assets/dist"), - } - - // Tries to open a database on the location provided. This - // function will automatically create a new one if it doesn't - // exist. - db, err := storm.Open(database) - if err != nil { - return nil, err - } + m.Assets = rice.MustFindBox("./assets/dist") + m.Cron = cron.New() // Tries to get the encryption key from the database. // If it doesn't exist, create a new one of 256 bits. - err = db.Get("config", "key", &m.key) - if err != nil && err == storm.ErrNotFound { + err := m.Store.Config.Get("key", &m.Key) + if err != nil && err == ErrNotExist { var bytes []byte - bytes, err = generateRandomBytes(64) + bytes, err = GenerateRandomBytes(64) if err != nil { - return nil, err + return err } - m.key = bytes - err = db.Set("config", "key", m.key) + m.Key = bytes + err = m.Store.Config.Save("key", m.Key) } if err != nil { - return nil, err + return err } // Tries to get the event commands from the database. // If they don't exist, initialize them. - err = db.Get("config", "commands", &m.Commands) + err = m.Store.Config.Get("commands", &m.Commands) if err != nil && err == storm.ErrNotFound { m.Commands = map[string][]string{ "before_save": {}, @@ -170,35 +144,29 @@ func New(database string, base User) (*FileManager, error) { "before_publish": {}, "after_publish": {}, } - err = db.Set("config", "commands", m.Commands) + err = m.Store.Config.Save("commands", m.Commands) } if err != nil { - return nil, err + return err } - // Tries to fetch the users from the database and if there are - // any, add them to the current File Manager instance. - var users []User - err = db.All(&users) + // Tries to fetch the users from the database. + users, err := m.Store.Users.Gets() if err != nil { - return nil, err - } - - for i := range users { - m.Users[users[i].Username] = &users[i] + return err } // If there are no users in the database, it creates a new one // based on 'base' User that must be provided by the function caller. if len(users) == 0 { - u := base + u := *m.DefaultUser u.Username = "admin" // Hashes the password. - u.Password, err = hashPassword("admin") + u.Password, err = HashPassword("admin") if err != nil { - return nil, err + return err } // The first user must be an administrator. @@ -209,26 +177,19 @@ func New(database string, base User) (*FileManager, error) { u.AllowPublish = true // Saves the user to the database. - if err := db.Save(&u); err != nil { - return nil, err + if err := m.Store.Users.Save(&u); err != nil { + return err } - - m.Users[u.Username] = &u } - // Attaches db to this File Manager instance. - m.db = db + m.DefaultUser.Username = "" + m.DefaultUser.Password = "" - // Create the default user, making a copy of the base. - base.Username = "" - base.Password = "" - m.DefaultUser = &base + m.Cron.AddFunc("@hourly", m.ShareCleaner) + m.Cron.Start() - m.cron.AddFunc("@hourly", m.shareCleaner) - m.cron.Start() - - return m, nil -} */ + return nil +} // RootURL returns the actual URL where // File Manager interface can be accessed. @@ -291,24 +252,19 @@ func (m *FileManager) Attach(s StaticGen) error { m.StaticGen = s - // TODO: Save... - /* err := m.db.Get("staticgen", "hugo", h) - if err != nil && err == storm.ErrNotFound { - err = m.db.Set("staticgen", "hugo", *h) + err = m.Store.Config.Get("staticgen_"+s.Name(), s) + if err == ErrNotExist { + return m.Store.Config.Save("staticgen_"+s.Name(), s) } - */ - return nil + + return err } -/* - -// shareCleaner removes sharing links that are no longer active. +// ShareCleaner removes sharing links that are no longer active. // This function is set to run periodically. -func (m FileManager) shareCleaner() { - var links []shareLink - +func (m FileManager) ShareCleaner() { // Get all links. - err := m.db.All(&links) + links, err := m.Store.Share.Gets() if err != nil { log.Print(err) return @@ -317,13 +273,13 @@ func (m FileManager) shareCleaner() { // Find the expired ones. for i := range links { if links[i].Expires && links[i].ExpireDate.Before(time.Now()) { - err = m.db.DeleteStruct(&links[i]) + err = m.Store.Share.Delete(links[i].Hash) if err != nil { log.Print(err) } } } -} */ +} // Runner runs the commands for a certain event type. func (m FileManager) Runner(event string, path string) error { diff --git a/http/auth.go b/http/auth.go index c5888e6e..ffade7ae 100644 --- a/http/auth.go +++ b/http/auth.go @@ -1,14 +1,11 @@ package http import ( - "crypto/rand" "encoding/json" "net/http" "strings" "time" - "golang.org/x/crypto/bcrypt" - jwt "github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go/request" fm "github.com/hacdias/filemanager" @@ -33,13 +30,13 @@ func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, er } // Checks if the user exists. - u, ok := c.Users[cred.Username] - if !ok { + u, err := c.Store.Users.Get(cred.ID) + if err != nil { return http.StatusForbidden, nil } // Checks if the password is correct. - if !checkPasswordHash(cred.Password, u.Password) { + if !fm.CheckPasswordHash(cred.Password, u.Password) { return http.StatusForbidden, nil } @@ -86,7 +83,7 @@ func printToken(c *fm.Context, w http.ResponseWriter) (int, error) { // Creates the token and signs it. token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString(c.key) + signed, err := token.SignedString(c.Key) if err != nil { return http.StatusInternalServerError, err @@ -127,7 +124,7 @@ func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) { } keyFunc := func(token *jwt.Token) (interface{}, error) { - return c.key, nil + return c.Key, nil } var claims claims token, err := request.ParseFromRequestWithClaims(r, @@ -140,38 +137,11 @@ func validateAuth(c *fm.Context, r *http.Request) (bool, *fm.User) { return false, nil } - u, ok := c.Users[claims.User.Username] - if !ok { + u, err := c.Store.Users.Get(claims.User.ID) + if err != nil { return false, nil } c.User = u return true, u } - -// hashPassword generates an hash from a password using bcrypt. -func hashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(bytes), err -} - -// checkPasswordHash compares a password with an hash to check if they match. -func checkPasswordHash(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -// generateRandomBytes returns securely generated random bytes. -// It will return an error if the system's secure random -// number generator fails to function correctly, in which -// case the caller should not continue. -func generateRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - // Note that err == nil only if we read len(b) bytes. - if err != nil { - return nil, err - } - - return b, nil -} diff --git a/http/auth_test.go b/http/auth_test.go.txt similarity index 97% rename from http/auth_test.go rename to http/auth_test.go.txt index ee6ce8a5..fd8313ba 100644 --- a/http/auth_test.go +++ b/http/auth_test.go.txt @@ -45,7 +45,7 @@ func TestRenewHandler(t *testing.T) { // First, we have to make an auth request to get the user authenticated, r, err := http.NewRequest("POST", "/api/auth/get", strings.NewReader(defaultCredentials)) if err != nil { - t.Fatal(err) + t.Fatal(fm.Err) } w := httptest.NewRecorder() @@ -60,7 +60,7 @@ func TestRenewHandler(t *testing.T) { // Test renew authorization via Authorization Header. r, err = http.NewRequest("GET", "/api/auth/renew", nil) if err != nil { - t.Fatal(err) + t.Fatal(fm.Err) } r.Header.Set("Authorization", "Bearer "+token) @@ -74,7 +74,7 @@ func TestRenewHandler(t *testing.T) { // Test renew authorization via cookie field. r, err = http.NewRequest("GET", "/api/auth/renew", nil) if err != nil { - t.Fatal(err) + t.Fatal(fm.Err) } r.AddCookie(&http.Cookie{ diff --git a/http/http.go b/http/http.go index cc2291be..23653720 100644 --- a/http/http.go +++ b/http/http.go @@ -2,7 +2,6 @@ package http import ( "encoding/json" - "errors" "html/template" "net/http" "os" @@ -13,21 +12,10 @@ import ( fm "github.com/hacdias/filemanager" ) -var ( - errUserExist = errors.New("user already exists") - errUserNotExist = errors.New("user does not exist") - errEmptyRequest = errors.New("request body is empty") - errEmptyPassword = errors.New("password is empty") - errEmptyUsername = errors.New("username is empty") - errEmptyScope = errors.New("scope is empty") - errWrongDataType = errors.New("wrong data type") - errInvalidUpdateField = errors.New("invalid field to update") -) - // ServeHTTP is the main entry point of this HTML application. func ServeHTTP(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { // Checks if the URL contains the baseURL and strips it. Otherwise, it just - // returns a 404 error because we're not supposed to be here! + // returns a 404 fm.Error because we're not supposed to be here! p := strings.TrimPrefix(r.URL.Path, c.BaseURL) if len(p) >= len(r.URL.Path) && c.BaseURL != "" { @@ -41,7 +29,7 @@ func ServeHTTP(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro if r.URL.Path == "/sw.js" { return renderFile( c, w, - c.assets.MustString("sw.js"), + c.Assets.MustString("sw.js"), "application/javascript", ) } @@ -83,7 +71,7 @@ func ServeHTTP(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro return renderFile( c, w, - c.assets.MustString("index.html"), + c.Assets.MustString("index.html"), "text/html", ) } @@ -91,13 +79,13 @@ func ServeHTTP(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro // staticHandler handles the static assets path. func staticHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { if r.URL.Path != "/static/manifest.json" { - http.FileServer(c.assets.HTTPBox()).ServeHTTP(w, r) + http.FileServer(c.Assets.HTTPBox()).ServeHTTP(w, r) return 0, nil } return renderFile( c, w, - c.assets.MustString("static/manifest.json"), + c.Assets.MustString("static/manifest.json"), "application/json", ) } @@ -141,7 +129,7 @@ func apiHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, err var err error c.File, err = fm.GetInfo(r.URL, c.FileManager, c.User) if err != nil { - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } } @@ -177,7 +165,7 @@ func checksumHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int query := r.URL.Query().Get("algo") val, err := c.File.Checksum(query) - if err == errInvalidOption { + if err == fm.ErrInvalidOption { return http.StatusBadRequest, err } else if err != nil { return http.StatusInternalServerError, err @@ -211,7 +199,7 @@ func renderFile(c *fm.Context, w http.ResponseWriter, file string, contentType s err := tpl.Execute(w, map[string]interface{}{ "BaseURL": c.RootURL(), - "StaticGen": c.staticgen, + "StaticGen": c.StaticGen.Name(), }) if err != nil { return http.StatusInternalServerError, err @@ -221,12 +209,11 @@ func renderFile(c *fm.Context, w http.ResponseWriter, file string, contentType s } func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - var s shareLink - err := c.db.One("Hash", r.URL.Path, &s) + s, err := c.Store.Share.Get(r.URL.Path) if err == storm.ErrNotFound { return renderFile( c, w, - c.assets.MustString("static/share/404.html"), + c.Assets.MustString("static/share/404.html"), "text/html", ) } @@ -236,10 +223,10 @@ func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro } if s.Expires && s.ExpireDate.Before(time.Now()) { - c.db.DeleteStruct(&s) + c.Store.Share.Delete(s.Hash) return renderFile( c, w, - c.assets.MustString("static/share/404.html"), + c.Assets.MustString("static/share/404.html"), "text/html", ) } @@ -248,10 +235,10 @@ func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro info, err := os.Stat(s.Path) if err != nil { - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } - c.File = &file{ + c.File = &fm.File{ Path: s.Path, Name: info.Name(), ModTime: info.ModTime(), @@ -263,7 +250,7 @@ func sharePage(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, erro dl := r.URL.Query().Get("dl") if dl == "" || dl == "0" { - tpl := template.Must(template.New("file").Parse(c.assets.MustString("static/share/index.html"))) + tpl := template.Must(template.New("file").Parse(c.Assets.MustString("static/share/index.html"))) w.Header().Set("Content-Type", "text/html; charset=utf-8") err := tpl.Execute(w, map[string]interface{}{ @@ -303,8 +290,8 @@ func matchURL(first, second string) bool { return strings.HasPrefix(first, second) } -// errorToHTTP converts errors to HTTP Status Code. -func errorToHTTP(err error, gone bool) int { +// ErrorToHTTP converts errors to HTTP Status Code. +func ErrorToHTTP(err error, gone bool) int { switch { case err == nil: return http.StatusOK diff --git a/http/resource.go b/http/resource.go index 0a5c5271..f983f05f 100644 --- a/http/resource.go +++ b/http/resource.go @@ -64,9 +64,9 @@ func resourceHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { // Gets the information of the directory/file. - f, err := getInfo(r.URL, c.FileManager, c.User) + f, err := fm.GetInfo(r.URL, c.FileManager, c.User) if err != nil { - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } // If it's a dir and the path doesn't end with a trailing slash, @@ -83,7 +83,7 @@ func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) ( // Tries to get the file type. if err = f.GetFileType(true); err != nil { - return errorToHTTP(err, true), err + return ErrorToHTTP(err, true), err } // Serve a preview if the file can't be edited or the @@ -97,7 +97,7 @@ func resourceGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) ( f.Kind = "editor" // Tries to get the editor data. - if err = f.getEditor(); err != nil { + if err = f.GetEditor(); err != nil { return http.StatusInternalServerError, err } @@ -109,11 +109,11 @@ func listingHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, f.Kind = "listing" // Tries to get the listing data. - if err := f.getListing(c, r); err != nil { - return errorToHTTP(err, true), err + if err := f.GetListing(c.User, r); err != nil { + return ErrorToHTTP(err, true), err } - listing := f.listing + listing := f.Listing // Defines the cookie scope. cookieScope := c.RootURL() @@ -144,7 +144,7 @@ func resourceDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request // Remove the file or folder. err := c.User.FileSystem.RemoveAll(r.URL.Path) if err != nil { - return errorToHTTP(err, true), err + return ErrorToHTTP(err, true), err } return http.StatusOK, nil @@ -160,7 +160,7 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques } // Discard any invalid upload before returning to avoid connection - // reset error. + // reset fm.Error. defer func() { io.Copy(ioutil.Discard, r.Body) }() @@ -175,13 +175,13 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques // Otherwise we try to create the directory. err := c.User.FileSystem.Mkdir(r.URL.Path, 0776) - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } // If using POST method, we are trying to create a new file so it is not - // desirable to override an already existent file. Thus, we check + // desirable to ovfm.Erride an already existent file. Thus, we check // if the file already exists. If so, we just return a 409 Conflict. - if r.Method == http.MethodPost && r.Header.Get("Action") != "override" { + if r.Method == http.MethodPost && r.Header.Get("Action") != "ovfm.Erride" { if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { return http.StatusConflict, errors.New("There is already a file on that path") } @@ -190,20 +190,20 @@ func resourcePostPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Reques // Create/Open the file. f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0776) if err != nil { - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } defer f.Close() // Copies the new content for the file. _, err = io.Copy(f, r.Body) if err != nil { - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } // Gets the info about the file. fi, err := f.Stat() if err != nil { - return errorToHTTP(err, false), err + return ErrorToHTTP(err, false), err } // Check if this instance has a Static Generator and handles publishing @@ -242,7 +242,7 @@ func resourcePublishSchedule(c *fm.Context, w http.ResponseWriter, r *http.Reque return http.StatusInternalServerError, err } - c.cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { + c.Cron.AddFunc(t.Format("05 04 15 02 01 *"), func() { _, err := resourcePublish(c, w, r) if err != nil { log.Print(err) @@ -283,7 +283,7 @@ func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) action := r.Header.Get("Action") dst, err := url.QueryUnescape(dst) if err != nil { - return errorToHTTP(err, true), err + return ErrorToHTTP(err, true), err } src := r.URL.Path @@ -298,7 +298,7 @@ func resourcePatchHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) err = c.User.FileSystem.Rename(src, dst) } - return errorToHTTP(err, true), err + return ErrorToHTTP(err, true), err } // displayMode obtains the display mode from the Cookie. diff --git a/http/settings.go b/http/settings.go index 887dd6d6..f0d9b2a3 100644 --- a/http/settings.go +++ b/http/settings.go @@ -27,7 +27,7 @@ type option struct { func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) { // Checks if the request body is empty. if r.Body == nil { - return nil, errEmptyRequest + return nil, fm.ErrEmptyRequest } // Parses the request body and checks if it's well formed. @@ -39,7 +39,7 @@ func parsePutSettingsRequest(r *http.Request) (*modifySettingsRequest, error) { // Checks if the request type is right. if mod.What != "settings" { - return nil, errWrongDataType + return nil, fm.ErrWrongDataType } return mod, nil @@ -103,9 +103,10 @@ func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) ( if err != nil { return http.StatusBadRequest, err } + // Update the commands. if mod.Which == "commands" { - if err := c.db.Set("config", "commands", mod.Data.Commands); err != nil { + if err := c.Store.Config.Save("commands", mod.Data.Commands); err != nil { return http.StatusInternalServerError, err } @@ -120,7 +121,7 @@ func settingsPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) ( return http.StatusInternalServerError, err } - err = c.db.Set("staticgen", c.staticgen, c.StaticGen) + err = c.Store.Config.Save("staticgen_"+c.StaticGen.Name(), c.StaticGen) if err != nil { return http.StatusInternalServerError, err } diff --git a/http/share.go b/http/share.go index 8a628c48..0f9fe06e 100644 --- a/http/share.go +++ b/http/share.go @@ -13,13 +13,6 @@ import ( fm "github.com/hacdias/filemanager" ) -type shareLink struct { - Hash string `json:"hash" storm:"id,index"` - Path string `json:"path" storm:"index"` - Expires bool `json:"expires"` - ExpireDate time.Time `json:"expireDate"` -} - func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { r.URL.Path = sanitizeURL(r.URL.Path) @@ -36,12 +29,8 @@ func shareHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, e } func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - var ( - s []*shareLink - path = filepath.Join(string(c.User.FileSystem), r.URL.Path) - ) - - err := c.db.Find("Path", path, &s) + path := filepath.Join(string(c.User.FileSystem), r.URL.Path) + s, err := c.Store.Share.GetByPath(path) if err == storm.ErrNotFound { return http.StatusNotFound, nil } @@ -52,7 +41,7 @@ func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int for i, link := range s { if link.Expires && link.ExpireDate.Before(time.Now()) { - c.db.DeleteStruct(&shareLink{Hash: link.Hash}) + c.Store.Share.Delete(link.Hash) s = append(s[:i], s[i+1:]...) } } @@ -63,7 +52,7 @@ func shareGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { path := filepath.Join(string(c.User.FileSystem), r.URL.Path) - var s shareLink + var s fm.ShareLink expire := r.URL.Query().Get("expires") unit := r.URL.Query().Get("unit") @@ -75,14 +64,14 @@ func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in } } - bytes, err := generateRandomBytes(32) + bytes, err := fm.GenerateRandomBytes(32) if err != nil { return http.StatusInternalServerError, err } str := hex.EncodeToString(bytes) - s = shareLink{ + s = fm.ShareLink{ Path: path, Hash: str, Expires: expire != "", @@ -109,8 +98,7 @@ func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in s.ExpireDate = time.Now().Add(add) } - err = c.db.Save(&s) - if err != nil { + if err := c.Store.Share.Save(&s); err != nil { return http.StatusInternalServerError, err } @@ -118,9 +106,7 @@ func sharePostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in } func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - var s shareLink - - err := c.db.One("Hash", strings.TrimPrefix(r.URL.Path, "/"), &s) + s, err := c.Store.Share.Get(strings.TrimPrefix(r.URL.Path, "/")) if err == storm.ErrNotFound { return http.StatusNotFound, nil } @@ -129,7 +115,7 @@ func shareDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) ( return http.StatusInternalServerError, err } - err = c.db.DeleteStruct(&s) + err = c.Store.Share.Delete(s.Hash) if err != nil { return http.StatusInternalServerError, err } diff --git a/http/users.go b/http/users.go index 65384c7d..58fb0ed6 100644 --- a/http/users.go +++ b/http/users.go @@ -9,7 +9,6 @@ import ( "strconv" "strings" - "github.com/asdine/storm" fm "github.com/hacdias/filemanager" ) @@ -48,7 +47,7 @@ func usersHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, e // getUserID returns the id from the user which is present // in the request url. If the url is invalid and doesn't -// contain a valid ID, it returns an error. +// contain a valid ID, it returns an fm.Error. func getUserID(r *http.Request) (int, error) { // Obtains the ID in string from the URL and converts // it into an integer. @@ -64,11 +63,11 @@ func getUserID(r *http.Request) (int, error) { // getUser returns the user which is present in the request // body. If the body is empty or the JSON is invalid, it -// returns an error. +// returns an fm.Error. func getUser(r *http.Request) (*fm.User, string, error) { // Checks if the request body is empty. if r.Body == nil { - return nil, "", errEmptyRequest + return nil, "", fm.ErrEmptyRequest } // Parses the request body and checks if it's well formed. @@ -80,7 +79,7 @@ func getUser(r *http.Request) (*fm.User, string, error) { // Checks if the request type is right. if mod.What != "user" { - return nil, "", errWrongDataType + return nil, "", fm.ErrWrongDataType } return mod.Data, mod.Which, nil @@ -94,15 +93,15 @@ func usersGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int // Request for the listing of users. if r.URL.Path == "/" { - users := []User{} + users, err := c.Store.Users.Gets() + if err != nil { + return http.StatusInternalServerError, err + } - for _, user := range c.Users { - // Copies the user info and removes its - // password so it won't be sent to the - // front-end. - u := *user + for _, u := range users { + // Removes the user password so it won't + // be sent to the front-end. u.Password = "" - users = append(users, u) } sort.Slice(users, func(i, j int) bool { @@ -117,19 +116,17 @@ func usersGetHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int return http.StatusInternalServerError, err } - // Searches for the user and prints the one who matches. - for _, user := range c.Users { - if user.ID != id { - continue - } - - u := *user - u.Password = "" - return renderJSON(w, u) + u, err := c.Store.Users.Get(id) + if err == fm.ErrExist { + return http.StatusNotFound, err } - // If there aren't any matches, return not found. - return http.StatusNotFound, errUserNotExist + if err != nil { + return http.StatusInternalServerError, err + } + + u.Password = "" + return renderJSON(w, u) } func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { @@ -144,17 +141,17 @@ func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in // Checks if username isn't empty. if u.Username == "" { - return http.StatusBadRequest, errEmptyUsername + return http.StatusBadRequest, fm.ErrEmptyUsername } // Checks if filesystem isn't empty. if u.FileSystem == "" { - return http.StatusBadRequest, errEmptyScope + return http.StatusBadRequest, fm.ErrEmptyScope } // Checks if password isn't empty. if u.Password == "" { - return http.StatusBadRequest, errEmptyPassword + return http.StatusBadRequest, fm.ErrEmptyPassword } // The username, password and scope cannot be empty. @@ -164,7 +161,7 @@ func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in // Initialize rules if they're not initialized. if u.Rules == nil { - u.Rules = []*Rule{} + u.Rules = []*fm.Rule{} } // Initialize commands if not initialized. @@ -183,7 +180,7 @@ func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in } // Hashes the password. - pw, err := hashPassword(u.Password) + pw, err := fm.HashPassword(u.Password) if err != nil { return http.StatusInternalServerError, err } @@ -191,18 +188,15 @@ func usersPostHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (in u.Password = pw // Saves the user to the database. - err = c.db.Save(u) - if err == storm.ErrAlreadyExists { - return http.StatusConflict, errUserExist + err = c.Store.Users.Save(u) + if err == fm.ErrExist { + return http.StatusConflict, err } if err != nil { return http.StatusInternalServerError, err } - // Saves the user to the memory. - c.Users[u.Username] = u - // Set the Location header and return. w.Header().Set("Location", "/users/"+strconv.Itoa(u.ID)) w.WriteHeader(http.StatusCreated) @@ -243,23 +237,15 @@ func usersDeleteHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) ( } // Deletes the user from the database. - err = c.db.DeleteStruct(&User{ID: id}) - if err == storm.ErrNotFound { - return http.StatusNotFound, errUserNotExist + err = c.Store.Users.Delete(id) + if err == fm.ErrNotExist { + return http.StatusNotFound, fm.ErrNotExist } if err != nil { return http.StatusInternalServerError, err } - // Delete the user from the in-memory users map. - for _, user := range c.Users { - if user.ID == id { - delete(c.Users, user.Username) - break - } - } - return http.StatusOK, nil } @@ -290,12 +276,8 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int if which == "partial" { c.User.CSS = u.CSS c.User.Locale = u.Locale - err = c.db.UpdateField(&User{ID: c.User.ID}, "CSS", u.CSS) - if err != nil { - return http.StatusInternalServerError, err - } - err = c.db.UpdateField(&User{ID: c.User.ID}, "Locale", u.Locale) + err = c.Store.Users.Update(c.User, "CSS", "Locale") if err != nil { return http.StatusInternalServerError, err } @@ -306,16 +288,15 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int // Updates the Password. if which == "password" { if u.Password == "" { - return http.StatusBadRequest, errEmptyPassword + return http.StatusBadRequest, fm.ErrEmptyPassword } - pw, err := hashPassword(u.Password) + c.User.Password, err = fm.HashPassword(u.Password) if err != nil { return http.StatusInternalServerError, err } - c.User.Password = pw - err = c.db.UpdateField(&User{ID: c.User.ID}, "Password", pw) + err = c.Store.Users.Update(c.User, "Password") if err != nil { return http.StatusInternalServerError, err } @@ -325,17 +306,17 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int // If can only be all. if which != "all" { - return http.StatusBadRequest, errInvalidUpdateField + return http.StatusBadRequest, fm.ErrInvalidUpdateField } // Checks if username isn't empty. if u.Username == "" { - return http.StatusBadRequest, errEmptyUsername + return http.StatusBadRequest, fm.ErrEmptyUsername } // Checks if filesystem isn't empty. if u.FileSystem == "" { - return http.StatusBadRequest, errEmptyScope + return http.StatusBadRequest, fm.ErrEmptyScope } // Checks if the scope exists. @@ -345,7 +326,7 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int // Initialize rules if they're not initialized. if u.Rules == nil { - u.Rules = []*Rule{} + u.Rules = []*fm.Rule{} } // Initialize commands if not initialized. @@ -354,22 +335,20 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int } // Gets the current saved user from the in-memory map. - var suser *User - for _, user := range c.Users { - if user.ID == id { - suser = user - break - } - } - if suser == nil { + suser, err := c.Store.Users.Get(id) + if err == fm.ErrNotExist { return http.StatusNotFound, nil } + if err != nil { + return http.StatusInternalServerError, err + } + u.ID = id // Changes the password if the request wants it. if u.Password != "" { - pw, err := hashPassword(u.Password) + pw, err := fm.HashPassword(u.Password) if err != nil { return http.StatusInternalServerError, err } @@ -381,17 +360,10 @@ func usersPutHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int // Updates the whole User struct because we always are supposed // to send a new entire object. - err = c.db.Save(u) + err = c.Store.Users.Update(u) if err != nil { return http.StatusInternalServerError, err } - // If the user changed the username, delete the old user - // from the in-memory user map. - if suser.Username != u.Username { - delete(c.Users, suser.Username) - } - - c.Users[u.Username] = u return http.StatusOK, nil } diff --git a/http/websockets.go b/http/websockets.go index c54818fe..c12cc4ba 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -28,7 +28,7 @@ var ( // command handles the requests for VCS related commands: git, svn and mercurial func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Upgrades the connection to a websocket and checks for errors. + // Upgrades the connection to a websocket and checks for fm.Errors. conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return 0, err @@ -92,7 +92,7 @@ func command(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) cmd.Stderr = buff cmd.Stdout = buff - // Starts the command and checks for errors. + // Starts the command and checks for fm.Errors. err = cmd.Start() if err != nil { return http.StatusInternalServerError, err @@ -241,7 +241,7 @@ func parseSearch(value string) *searchOptions { // search searches for a file or directory. func search(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) { - // Upgrades the connection to a websocket and checks for errors. + // Upgrades the connection to a websocket and checks for fm.Errors. conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return 0, err diff --git a/user.go b/user.go index 3bc4ecd8..18267b21 100644 --- a/user.go +++ b/user.go @@ -1,12 +1,28 @@ package filemanager import ( + "crypto/rand" + "errors" "regexp" "strings" + "time" + + "golang.org/x/crypto/bcrypt" "github.com/hacdias/fileutils" ) +var ( + ErrExist = errors.New("the resource already exists") + ErrNotExist = errors.New("the resource does not exist") + ErrEmptyRequest = errors.New("request body is empty") + ErrEmptyPassword = errors.New("password is empty") + ErrEmptyUsername = errors.New("username is empty") + ErrEmptyScope = errors.New("scope is empty") + ErrWrongDataType = errors.New("wrong data type") + ErrInvalidUpdateField = errors.New("invalid field to update") +) + // DefaultUser is used on New, when no 'base' user is provided. var DefaultUser = User{ AllowCommands: true, @@ -110,10 +126,24 @@ func (r *Regexp) MatchString(s string) bool { return r.regexp.MatchString(s) } +type ShareLink struct { + Hash string `json:"hash" storm:"id,index"` + Path string `json:"path" storm:"index"` + Expires bool `json:"expires"` + ExpireDate time.Time `json:"expireDate"` +} + +type Store struct { + Users UsersStore + Config ConfigStore + Share ShareStore +} + type UsersStore interface { Get(id int) (*User, error) Gets() ([]*User, error) - Save(u *User, fields ...string) error + Save(u *User) error + Update(u *User, fields ...string) error Delete(id int) error } @@ -123,6 +153,36 @@ type ConfigStore interface { } type ShareStore interface { - Get(hash string) - Save() + Get(hash string) (*ShareLink, error) + GetByPath(path string) ([]*ShareLink, error) + Gets() ([]*ShareLink, error) + Save(s *ShareLink) error + Delete(hash string) error +} + +// HashPassword generates an hash from a password using bcrypt. +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPasswordHash compares a password with an hash to check if they match. +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// GenerateRandomBytes returns securely generated random bytes. +// It will return an fm.Error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil }