diff --git a/_remove/filebrowser.go b/_remove/filebrowser.go deleted file mode 100644 index 58540d8d..00000000 --- a/_remove/filebrowser.go +++ /dev/null @@ -1,185 +0,0 @@ -package lib - -/* -import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "encoding/hex" - "fmt" - "hash" - "io" - "log" - "mime" - "net/http" - "os" - "os/exec" - "path" - "path/filepath" - "strings" - "sync" - - "github.com/filebrowser/filebrowser/files" - "github.com/filebrowser/filebrowser/rules" - "github.com/filebrowser/filebrowser/settings" - "github.com/filebrowser/filebrowser/users" - "github.com/mholt/caddy" - "github.com/spf13/afero" -) - - - -// FileBrowser represents a File Browser instance which must -// be created through NewFileBrowser. -type FileBrowser struct { - settings *settings.Settings - mux sync.RWMutex -} - -// NewFileBrowser creates a new File Browser instance from a -// storage backend. If that backend doesn't contain settings -// on it (returns ErrNotExist), then we generate a new key -// and base settings. -func NewFileBrowser(backend StorageBackend) (*FileBrowser, error) { - set, err := backend.GetSettings() - - if err == ErrNotExist { - var key []byte - key, err = generateRandomBytes(64) - - if err != nil { - return nil, err - } - - set = &settings.Settings{Key: key} - err = backend.SaveSettings(set) - } - - if err != nil { - return nil, err - } - - return &FileBrowser{ - settings: set, - storage: backend, - }, nil -} - -// RLockSettings locks the settings for reading. -func (f *FileBrowser) RLockSettings() { - f.mux.RLock() -} - -// RUnlockSettings unlocks the settings for reading. -func (f *FileBrowser) RUnlockSettings() { - f.mux.RUnlock() -} - -// CheckRules matches the rules against user rules and global rules. -func (f *FileBrowser) CheckRules(path string, user *users.User) bool { - f.RLockSettings() - val := rules.Check(path, user, f.settings) - f.RUnlockSettings() - return val -} - -// RunHook runs the hooks for the before and after event. -func (f *FileBrowser) RunHook(fn func() error, evt, path, dst string, user *users.User) error { - path = user.FullPath(path) - dst = user.FullPath(dst) - - if val, ok := f.settings.Commands["before_"+evt]; ok { - for _, command := range val { - err := f.exec(command, "before_"+evt, path, dst, user) - if err != nil { - return err - } - } - } - - err := fn() - if err != nil { - return err - } - - if val, ok := f.settings.Commands["after_"+evt]; ok { - for _, command := range val { - err := f.exec(command, "after_"+evt, path, dst, user) - if err != nil { - return err - } - } - } - - return nil -} - -// ParseCommand parses the command taking in account if the current -// instance uses a shell to run the commands or just calls the binary -// directyly. -func (f *FileBrowser) ParseCommand(raw string) ([]string, error) { - f.RLockSettings() - defer f.RUnlockSettings() - - command := []string{} - - if len(f.settings.Shell) == 0 { - cmd, args, err := caddy.SplitCommandAndArgs(raw) - if err != nil { - return nil, err - } - - _, err = exec.LookPath(cmd) - if err != nil { - return nil, err - } - - command = append(command, cmd) - command = append(command, args...) - } else { - command = append(f.settings.Shell, raw) - } - - return command, nil -} - - - - - - - -func (f *FileBrowser) exec(raw, evt, path, dst string, user *users.User) error { - blocking := true - - if strings.HasSuffix(raw, "&") { - blocking = false - raw = strings.TrimSpace(strings.TrimSuffix(raw, "&")) - } - - command, err := f.ParseCommand(raw) - if err != nil { - return err - } - - cmd := exec.Command(command[0], command[1:]...) - cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) - cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) - cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt)) - cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) - cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst)) - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if !blocking { - log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " ")) - return cmd.Start() - } - - log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " ")) - return cmd.Run() -} -*/ diff --git a/errors/errors.go b/errors/errors.go index ef178539..e989faf2 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -8,6 +8,7 @@ var ( ErrNotExist = errors.New("the resource does not exist") ErrEmptyPassword = errors.New("password is empty") ErrEmptyUsername = errors.New("username is empty") + ErrEmptyRequest = errors.New("empty request") ErrScopeIsRelative = errors.New("scope is a relative path") ErrInvalidDataType = errors.New("invalid data type") ErrIsDirectory = errors.New("file is directory") diff --git a/http/auth.go b/http/auth.go index d3792ca5..9fd357c0 100644 --- a/http/auth.go +++ b/http/auth.go @@ -1,7 +1,6 @@ package http import ( - "context" "encoding/json" "net/http" "os" @@ -14,76 +13,6 @@ import ( "github.com/filebrowser/filebrowser/users" ) -func (e *env) loginHandler(w http.ResponseWriter, r *http.Request) { - user, err := e.auther.Auth(r) - if err == os.ErrPermission { - httpErr(w, r, http.StatusForbidden, nil) - } else if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - } else { - e.printToken(w, r, user) - } -} - -type signupBody struct { - Username string `json:"username"` - Password string `json:"password"` -} - -func (e *env) signupHandler(w http.ResponseWriter, r *http.Request) { - settings, err := e.Settings.Get() - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - - if !settings.Signup { - httpErr(w, r, http.StatusForbidden, nil) - return - } - - if r.Body == nil { - httpErr(w, r, http.StatusBadRequest, nil) - return - } - - info := &signupBody{} - err = json.NewDecoder(r.Body).Decode(info) - if err != nil { - httpErr(w, r, http.StatusBadRequest, nil) - return - } - - if info.Password == "" || info.Username == "" { - httpErr(w, r, http.StatusBadRequest, nil) - return - } - - user := &users.User{ - Username: info.Username, - } - - settings.Defaults.Apply(user) - - pwd, err := users.HashPwd(info.Password) - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - - user.Password = pwd - err = e.Users.Save(user) - if err == errors.ErrExist { - httpErr(w, r, http.StatusConflict, nil) - return - } else if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - - httpErr(w, r, http.StatusOK, nil) -} - type userInfo struct { ID uint `json:"id"` Locale string `json:"locale"` @@ -118,28 +47,17 @@ func (e extractor) ExtractToken(r *http.Request) (string, error) { return auth, nil } -func (e *env) auth(next http.HandlerFunc) http.HandlerFunc { - keyFunc := func(token *jwt.Token) (interface{}, error) { - settings, err := e.Settings.Get() - if err != nil { - return nil, err +func withUser(fn handleFunc) handleFunc { + return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + keyFunc := func(token *jwt.Token) (interface{}, error) { + return d.settings.Key, nil } - return settings.Key, nil - } - - nextWithUser := func(w http.ResponseWriter, r *http.Request, id uint) { - ctx := context.WithValue(r.Context(), keyUserID, id) - next(w, r.WithContext(ctx)) - } - - return func(w http.ResponseWriter, r *http.Request) { var tk authToken token, err := request.ParseFromRequestWithClaims(r, &extractor{}, &tk, keyFunc) if err != nil || !token.Valid { - httpErr(w, r, http.StatusForbidden, nil) - return + return http.StatusForbidden, nil } if !tk.VerifyExpiresAt(time.Now().Add(time.Hour).Unix(), true) { @@ -147,20 +65,88 @@ func (e *env) auth(next http.HandlerFunc) http.HandlerFunc { w.Header().Add("X-Renew-Token", "true") } - nextWithUser(w, r, tk.User.ID) + d.user, err = d.store.Users.Get(tk.User.ID) + return fn(w, r, d) } } -func (e *env) renew(w http.ResponseWriter, r *http.Request) { - user, ok := e.getUser(w, r) - if !ok { - return - } +func withAdmin(fn handleFunc) handleFunc { + return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if !d.user.Perm.Admin { + return http.StatusForbidden, nil + } - e.printToken(w, r, user) + return fn(w, r, d) + }) } -func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *users.User) { +var loginHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + auther, err := d.store.Auth.Get(d.settings.AuthMethod) + if err != nil { + return http.StatusInternalServerError, err + } + + user, err := auther.Auth(r) + if err == os.ErrPermission { + return http.StatusForbidden, nil + } else if err != nil { + return http.StatusInternalServerError, err + } else { + return printToken(w, r, d, user) + } +} + +type signupBody struct { + Username string `json:"username"` + Password string `json:"password"` +} + +var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if !d.settings.Signup { + return http.StatusMethodNotAllowed, nil + } + + if r.Body == nil { + return http.StatusBadRequest, nil + } + + info := &signupBody{} + err := json.NewDecoder(r.Body).Decode(info) + if err != nil { + return http.StatusBadRequest, err + } + + if info.Password == "" || info.Username == "" { + return http.StatusBadRequest, nil + } + + user := &users.User{ + Username: info.Username, + } + + d.settings.Defaults.Apply(user) + + pwd, err := users.HashPwd(info.Password) + if err != nil { + return http.StatusInternalServerError, err + } + + user.Password = pwd + err = d.store.Users.Save(user) + if err == errors.ErrExist { + return http.StatusConflict, err + } else if err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil +} + +var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + return printToken(w, r, d, d.user) +}) + +func printToken(w http.ResponseWriter, r *http.Request, d *data, user *users.User) (int, error) { claims := &authToken{ User: userInfo{ ID: user.ID, @@ -177,19 +163,12 @@ func (e *env) printToken(w http.ResponseWriter, r *http.Request, user *users.Use } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - settings, err := e.Settings.Get() + signed, err := token.SignedString(d.settings.Key) if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } - signed, err := token.SignedString(settings.Key) - - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - } else { - w.Header().Set("Content-Type", "cty") - w.Write([]byte(signed)) - } + w.Header().Set("Content-Type", "cty") + w.Write([]byte(signed)) + return 0, nil } diff --git a/http/data.go b/http/data.go new file mode 100644 index 00000000..235a1dd5 --- /dev/null +++ b/http/data.go @@ -0,0 +1,70 @@ +package http + +import ( + "log" + "net/http" + "strconv" + + "github.com/filebrowser/filebrowser/runner" + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/storage" + "github.com/filebrowser/filebrowser/users" +) + +type handleFunc func(w http.ResponseWriter, r *http.Request, d *data) (int, error) + +type data struct { + *runner.Runner + settings *settings.Settings + store *storage.Storage + user *users.User + raw interface{} +} + +// Check implements rules.Checker. +func (d *data) Check(path string) bool { + for _, rule := range d.user.Rules { + if rule.Matches(path) { + return rule.Allow + } + } + + for _, rule := range d.settings.Rules { + if rule.Matches(path) { + return rule.Allow + } + } + + return true +} + +func handle(fn handleFunc, prefix string, storage *storage.Storage) http.Handler { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + settings, err := storage.Settings.Get() + if err != nil { + log.Fatalln("ERROR: couldn't get settings") + return + } + + status, err := fn(w, r, &data{ + Runner: &runner.Runner{Settings: settings}, + store: storage, + settings: settings, + }) + + if status != 0 { + txt := http.StatusText(status) + http.Error(w, strconv.Itoa(status)+" "+txt, status) + } + + if status >= 400 || err != nil { + log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err) + } + }) + + if prefix == "" { + return handler + } + + return http.StripPrefix(prefix, handler) +} diff --git a/http/http.go b/http/http.go index f1936ffb..9b4c3cc4 100644 --- a/http/http.go +++ b/http/http.go @@ -1,24 +1,10 @@ package http import ( - "encoding/json" - "log" "net/http" - "strconv" - "time" - "github.com/filebrowser/filebrowser/auth" "github.com/filebrowser/filebrowser/storage" - "github.com/filebrowser/filebrowser/errors" - "github.com/filebrowser/filebrowser/users" "github.com/gorilla/mux" - "github.com/gorilla/websocket" -) - -type key int - -const ( - keyUserID key = iota ) type modifyRequest struct { @@ -26,67 +12,54 @@ type modifyRequest struct { Which []string `json:"which"` // Answer to: which fields? } -type env struct { - *storage.Storage - auther auth.Auther -} - -// NewHandler builds an HTTP handler on the top of a File Browser instance. func NewHandler(storage *storage.Storage) (http.Handler, error) { - /* authMethod := fb.GetSettings().AuthMethod - auther, err := fb.GetAuther(authMethod) - if err != nil { - return nil, err - } */ - - e := &env{} - r := mux.NewRouter() - index, static := e.getStaticHandlers() + /* index, static := e.getStaticHandlers() r.PathPrefix("/static").Handler(static) - r.NotFoundHandler = index + r.NotFoundHandler = index */ api := r.PathPrefix("/api").Subrouter() - api.HandleFunc("/login", e.loginHandler) - api.HandleFunc("/signup", e.signupHandler) - api.HandleFunc("/renew", e.auth(e.renew)) - users := api.PathPrefix("/users").Subrouter() + api.Handle("/login", handle(loginHandler, "", storage)) + api.Handle("/signup", handle(signupHandler, "", storage)) + api.Handle("/renew", handle(renewHandler, "", storage)) + + /* users := api.PathPrefix("/users").Subrouter() users.HandleFunc("", e.auth(e.usersGetHandler)).Methods("GET") users.HandleFunc("", e.auth(e.userPostHandler)).Methods("POST") users.HandleFunc("/{id:[0-9]+}", e.auth(e.userPutHandler)).Methods("PUT") users.HandleFunc("/{id:[0-9]+}", e.auth(e.userGetHandler)).Methods("GET") - users.HandleFunc("/{id:[0-9]+}", e.auth(e.userDeleteHandler)).Methods("DELETE") + users.HandleFunc("/{id:[0-9]+}", e.auth(e.userDeleteHandler)).Methods("DELETE") */ - api.PathPrefix("/resources").HandlerFunc(e.auth(e.resourceGetHandler)).Methods("GET") - api.PathPrefix("/resources").HandlerFunc(e.auth(e.resourceDeleteHandler)).Methods("DELETE") - api.PathPrefix("/resources").HandlerFunc(e.auth(e.resourcePostPutHandler)).Methods("POST") - api.PathPrefix("/resources").HandlerFunc(e.auth(e.resourcePostPutHandler)).Methods("PUT") - api.PathPrefix("/resources").HandlerFunc(e.auth(e.resourcePatchHandler)).Methods("PATCH") + api.PathPrefix("/resources").Handler(handle(resourceGetHandler, "/api/resources", storage)).Methods("GET") + api.PathPrefix("/resources").Handler(handle(resourceDeleteHandler, "/api/resources", storage)).Methods("DELETE") + api.PathPrefix("/resources").Handler(handle(resourcePostPutHandler, "/api/resources", storage)).Methods("POST") + api.PathPrefix("/resources").Handler(handle(resourcePostPutHandler, "/api/resources", storage)).Methods("PUT") + api.PathPrefix("/resources").Handler(handle(resourcePatchHandler, "/api/resources", storage)).Methods("PATCH") - api.PathPrefix("/share").HandlerFunc(e.auth(e.shareGetHandler)).Methods("GET") - api.PathPrefix("/share").HandlerFunc(e.auth(e.sharePostHandler)).Methods("POST") - api.PathPrefix("/share").HandlerFunc(e.auth(e.shareDeleteHandler)).Methods("DELETE") + api.PathPrefix("/share").Handler(handle(shareGetHandler, "/api/share", storage)).Methods("GET") + api.PathPrefix("/share").Handler(handle(sharePostHandler, "/api/share", storage)).Methods("POST") + api.PathPrefix("/share").Handler(handle(shareDeleteHandler, "/api/share", storage)).Methods("DELETE") - api.HandleFunc("/settings", e.auth(e.settingsGetHandler)).Methods("GET") - api.HandleFunc("/settings", e.auth(e.settingsPutHandler)).Methods("PUT") + api.Handle("/settings", handle(settingsGetHandler, "", storage)).Methods("GET") + api.Handle("/settings", handle(settingsPutHandler, "", storage)).Methods("PUT") - api.PathPrefix("/raw").HandlerFunc(e.auth(e.rawHandler)).Methods("GET") + /* api.PathPrefix("/raw").HandlerFunc(e.auth(e.rawHandler)).Methods("GET") api.PathPrefix("/command").HandlerFunc(e.auth(e.commandsHandler)) - api.PathPrefix("/search").HandlerFunc(e.auth(e.searchHandler)) + api.PathPrefix("/search").HandlerFunc(e.auth(e.searchHandler)) */ return r, nil } -func httpErr(w http.ResponseWriter, r *http.Request, status int, err error) { - txt := http.StatusText(status) - if err != nil || status >= 400 { - log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err) - } - http.Error(w, strconv.Itoa(status)+" "+txt, status) -} + +/* +type key int + + + + func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { txt := http.StatusText(status) @@ -95,46 +68,4 @@ func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { } ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second)) } - -func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) { - marsh, err := json.Marshal(data) - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - if _, err := w.Write(marsh); err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - } -} - -func (e *env) getUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) { - id := r.Context().Value(keyUserID).(uint) - user, err := e.Users.Get(id) - if err == errors.ErrNotExist { - httpErr(w, r, http.StatusForbidden, nil) - return nil, false - } - - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return nil, false - } - - return user, true -} - -func (e *env) getAdminUser(w http.ResponseWriter, r *http.Request) (*users.User, bool) { - user, ok := e.getUser(w, r) - if !ok { - return nil, false - } - - if !user.Perm.Admin { - httpErr(w, r, http.StatusForbidden, nil) - return nil, false - } - - return user, true -} +*/ diff --git a/http/raw.go b/http/raw.go index f96b1cf2..369231e0 100644 --- a/http/raw.go +++ b/http/raw.go @@ -1,18 +1,6 @@ package http -import ( - "errors" - "net/http" - "net/url" - "path/filepath" - "strings" - - "github.com/filebrowser/filebrowser/files" - "github.com/filebrowser/filebrowser/users" - "github.com/hacdias/fileutils" - "github.com/mholt/archiver" -) - +/* const apiRawPrefix = "/api/raw" func parseQueryFiles(r *http.Request, f *files.FileInfo, u *users.User) ([]string, error) { @@ -68,9 +56,7 @@ func (e *env) rawHandler(w http.ResponseWriter, r *http.Request) { return } - files.NewFileInfo(user.Fs, path, user.Perm.Modify) - - file, err := e.NewFile(path, user) + file, err := files.NewFileInfo(user.Fs, path, user.Perm.Modify) if err != nil { httpErr(w, r, httpFsErr(err), err) return @@ -160,4 +146,4 @@ func fileHandler(w http.ResponseWriter, r *http.Request, file *files.File, user } http.ServeContent(w, r, file.Name, file.ModTime, fd) -} +} */ diff --git a/http/resource.go b/http/resource.go index a34f6cf4..a5c2e020 100644 --- a/http/resource.go +++ b/http/resource.go @@ -9,116 +9,64 @@ import ( "os" "strings" + "github.com/filebrowser/filebrowser/files" + + "github.com/filebrowser/filebrowser/errors" "github.com/filebrowser/filebrowser/fileutils" - - "github.com/filebrowser/filebrowser/users" ) -const apiResourcePrefix = "/api/resources" +// TODO: trim path -func httpFsErr(err error) int { - switch { - case err == nil: - return http.StatusOK - case os.IsPermission(err): - return http.StatusForbidden - case os.IsNotExist(err): - return http.StatusNotFound - case os.IsExist(err): - return http.StatusConflict - default: - return http.StatusInternalServerError - } -} - -func (e *env) getResourceData(w http.ResponseWriter, r *http.Request, prefix string) (string, *users.User, bool) { - user, ok := e.getUser(w, r) - if !ok { - return "", nil, ok - } - - path := strings.TrimPrefix(r.URL.Path, prefix) - path = strings.TrimSuffix(path, "/") - if path == "" { - path = "/" - } - - return path, user, true -} - -func (e *env) resourceGetHandler(w http.ResponseWriter, r *http.Request) { - path, user, ok := e.getResourceData(w, r, apiResourcePrefix) - if !ok { - return - } - - file, err := e.NewFile(path, user) +var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + file, err := files.NewFileInfo(d.user.Fs, r.URL.Path, d.user.Perm.Modify, d) if err != nil { - httpErr(w, r, httpFsErr(err), err) - return + return errToStatus(err), err } if file.IsDir { - file.Listing.Sorting = user.Sorting + file.Listing.Sorting = d.user.Sorting file.Listing.ApplySort() - renderJSON(w, r, file) - return + return renderJSON(w, r, file) } if checksum := r.URL.Query().Get("checksum"); checksum != "" { - err = e.Checksum(file, user, checksum) - if err == lib.ErrInvalidOption { - httpErr(w, r, http.StatusBadRequest, nil) - return + err := file.Checksum(checksum) + if err == errors.ErrInvalidOption { + return http.StatusBadRequest, nil } else if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } // do not waste bandwidth if we just want the checksum file.Content = "" } - renderJSON(w, r, file) -} + return renderJSON(w, r, file) +}) -func (e *env) resourceDeleteHandler(w http.ResponseWriter, r *http.Request) { - path, user, ok := e.getResourceData(w, r, apiResourcePrefix) - if !ok { - return +var resourceDeleteHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if r.URL.Path == "/" || !d.user.Perm.Delete { + return http.StatusForbidden, nil } - if path == "/" || !user.Perm.Delete { - httpErr(w, r, http.StatusForbidden, nil) - return - } - - err := e.RunHook(func() error { - return user.Fs.RemoveAll(path) - }, "delete", path, "", user) + err := d.RunHook(func() error { + return d.user.Fs.RemoveAll(r.URL.Path) + }, "delete", r.URL.Path, "", d.user) if err != nil { - httpErr(w, r, httpFsErr(err), err) - return + return errToStatus(err), err } - w.WriteHeader(http.StatusOK) -} + return http.StatusOK, nil +}) -func (e *env) resourcePostPutHandler(w http.ResponseWriter, r *http.Request) { - path, user, ok := e.getResourceData(w, r, apiResourcePrefix) - if !ok { - return +var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if !d.user.Perm.Create && r.Method == http.MethodPost { + return http.StatusForbidden, nil } - if !user.Perm.Create && r.Method == http.MethodPost { - httpErr(w, r, http.StatusForbidden, nil) - return - } - - if !user.Perm.Modify && r.Method == http.MethodPut { - httpErr(w, r, http.StatusForbidden, nil) - return + if !d.user.Perm.Modify && r.Method == http.MethodPut { + return http.StatusForbidden, nil } defer func() { @@ -128,24 +76,21 @@ func (e *env) resourcePostPutHandler(w http.ResponseWriter, r *http.Request) { // For directories, only allow POST for creation. if strings.HasSuffix(r.URL.Path, "/") { if r.Method == http.MethodPut { - httpErr(w, r, http.StatusMethodNotAllowed, nil) - } else { - err := user.Fs.MkdirAll(path, 0775) - httpErr(w, r, httpFsErr(err), err) + return http.StatusMethodNotAllowed, nil } - return + err := d.user.Fs.MkdirAll(r.URL.Path, 0775) + return errToStatus(err), err } if r.Method == http.MethodPost && r.URL.Query().Get("override") != "true" { - if _, err := user.Fs.Stat(path); err == nil { - httpErr(w, r, http.StatusConflict, nil) - return + if _, err := d.user.Fs.Stat(r.URL.Path); err == nil { + return http.StatusConflict, nil } } - err := e.RunHook(func() error { - file, err := user.Fs.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) + err := d.RunHook(func() error { + file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775) if err != nil { return err } @@ -165,58 +110,45 @@ func (e *env) resourcePostPutHandler(w http.ResponseWriter, r *http.Request) { etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size()) w.Header().Set("ETag", etag) return nil - }, "upload", path, "", user) + }, "upload", r.URL.Path, "", d.user) - if err != nil { - httpErr(w, r, httpFsErr(err), err) - return - } - - httpErr(w, r, http.StatusOK, nil) -} - -func (e *env) resourcePatchHandler(w http.ResponseWriter, r *http.Request) { - src, user, ok := e.getResourceData(w, r, apiResourcePrefix) - if !ok { - return - } + return errToStatus(err), err +}) +var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + src := r.URL.Path dst := r.URL.Query().Get("destination") action := r.URL.Query().Get("action") dst, err := url.QueryUnescape(dst) if err != nil { - httpErr(w, r, httpFsErr(err), err) - return + return errToStatus(err), err } if dst == "/" || src == "/" { - httpErr(w, r, http.StatusForbidden, nil) - return + return http.StatusForbidden, nil } switch action { case "copy": - if !user.Perm.Create { - httpErr(w, r, http.StatusForbidden, nil) - return + if !d.user.Perm.Create { + return http.StatusForbidden, nil } case "rename": default: action = "rename" - if !user.Perm.Rename { - httpErr(w, r, http.StatusForbidden, nil) - return + if !d.user.Perm.Rename { + return http.StatusForbidden, nil } } - err = e.RunHook(func() error { + err = d.RunHook(func() error { if action == "copy" { - return fileutils.Copy(user.Fs, src, dst) + return fileutils.Copy(d.user.Fs, src, dst) } - return user.Fs.Rename(src, dst) - }, action, src, dst, user) + return d.user.Fs.Rename(src, dst) + }, action, src, dst, d.user) - httpErr(w, r, httpFsErr(err), err) -} + return errToStatus(err), err +}) diff --git a/http/settings.go b/http/settings.go index 05bed412..4200f0c1 100644 --- a/http/settings.go +++ b/http/settings.go @@ -17,63 +17,33 @@ type settingsData struct { Commands map[string][]string `json:"commands"` } -func (e *env) settingsGetHandler(w http.ResponseWriter, r *http.Request) { - _, ok := e.getAdminUser(w, r) - if !ok { - return - } - - settings, err := e.Settings.Get() - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - +var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { data := &settingsData{ - Signup: settings.Signup, - Defaults: settings.Defaults, - Rules: settings.Rules, - Branding: settings.Branding, - Shell: settings.Shell, - Commands: settings.Commands, + Signup: d.settings.Signup, + Defaults: d.settings.Defaults, + Rules: d.settings.Rules, + Branding: d.settings.Branding, + Shell: d.settings.Shell, + Commands: d.settings.Commands, } - renderJSON(w, r, data) -} - -func (e *env) settingsPutHandler(w http.ResponseWriter, r *http.Request) { - _, ok := e.getAdminUser(w, r) - if !ok { - return - } + return renderJSON(w, r, data) +}) +var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { req := &settingsData{} err := json.NewDecoder(r.Body).Decode(req) if err != nil { - httpErr(w, r, http.StatusBadRequest, err) - return + return http.StatusBadRequest, err } - settings, err := e.Settings.Get() - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } + d.settings.Signup = req.Signup + d.settings.Defaults = req.Defaults + d.settings.Rules = req.Rules + d.settings.Branding = req.Branding + d.settings.Shell = req.Shell + d.settings.Commands = req.Commands - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } - - settings.Signup = req.Signup - settings.Defaults = req.Defaults - settings.Rules = req.Rules - settings.Branding = req.Branding - settings.Shell = req.Shell - settings.Commands = req.Commands - - err = e.Settings.Save(settings) - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - } -} + err = d.store.Settings.Save(d.settings) + return errToStatus(err), err +}) diff --git a/http/share.go b/http/share.go index aaee18c7..91ba5a27 100644 --- a/http/share.go +++ b/http/share.go @@ -12,104 +12,72 @@ import ( "github.com/filebrowser/filebrowser/share" ) -const apiSharePrefix = "/api/share" +func withPermShare(fn handleFunc) handleFunc { + return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + if !d.user.Perm.Share { + return http.StatusForbidden, nil + } -func (e *env) getShareData(w http.ResponseWriter, r *http.Request, prefix string) (string, bool) { - relPath, user, ok := e.getResourceData(w, r, apiSharePrefix) - if !ok { - return "", false - } - - if !user.Perm.Share { - httpErr(w, r, http.StatusForbidden, nil) - return "", false - } - - return user.FullPath(relPath), ok + return fn(w, r, d) + }) } -func (e *env) shareGetHandler(w http.ResponseWriter, r *http.Request) { - path, ok := e.getShareData(w, r, apiSharePrefix) - if !ok { - return - } - - s, err := e.Share.Gets(path) +var shareGetHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + s, err := d.store.Share.Gets(r.URL.Path) if err == errors.ErrNotExist { - renderJSON(w, r, []*share.Link{}) - return + return renderJSON(w, r, []*share.Link{}) } if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } for i, link := range s { if link.Expires && link.ExpireDate.Before(time.Now()) { - e.Share.Delete(link.Hash) + d.store.Share.Delete(link.Hash) s = append(s[:i], s[i+1:]...) } } - renderJSON(w, r, s) -} + return renderJSON(w, r, s) +}) -func (e *env) shareDeleteHandler(w http.ResponseWriter, r *http.Request) { - user, ok := e.getUser(w, r) - if !ok { - return - } - - if !user.Perm.Share { - httpErr(w, r, http.StatusForbidden, nil) - return - } - - hash := strings.TrimPrefix(r.URL.Path, apiSharePrefix) - hash = strings.TrimSuffix(hash, "/") +var shareDeleteHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + hash := strings.TrimSuffix(r.URL.Path, "/") hash = strings.TrimPrefix(hash, "/") + if hash == "" { - return + return http.StatusBadRequest, nil } - err := e.Share.Delete(hash) - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return - } -} + err := d.store.Share.Delete(hash) + return errToStatus(err), err +}) -func (e *env) sharePostHandler(w http.ResponseWriter, r *http.Request) { - path, ok := e.getShareData(w, r, apiSharePrefix) - if !ok { - return - } - - var s *lib.ShareLink +var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + var s *share.Link expire := r.URL.Query().Get("expires") unit := r.URL.Query().Get("unit") if expire == "" { var err error - s, err = e.GetLinkPermanent(path) + s, err = d.store.Share.GetPermanent(r.URL.Path) if err == nil { - w.Write([]byte(e.GetSettings().BaseURL + "/share/" + s.Hash)) - return + w.Write([]byte(d.settings.BaseURL + "/share/" + s.Hash)) + return 0, nil } } bytes := make([]byte, 6) _, err := rand.Read(bytes) if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } str := base64.URLEncoding.EncodeToString(bytes) - s = &lib.ShareLink{ - Path: path, + s = &share.Link{ + Path: r.URL.Path, Hash: str, Expires: expire != "", } @@ -117,8 +85,7 @@ func (e *env) sharePostHandler(w http.ResponseWriter, r *http.Request) { if expire != "" { num, err := strconv.Atoi(expire) if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } var add time.Duration @@ -136,10 +103,9 @@ func (e *env) sharePostHandler(w http.ResponseWriter, r *http.Request) { s.ExpireDate = time.Now().Add(add) } - if err := e.SaveLink(s); err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + if err := d.store.Share.Save(s); err != nil { + return http.StatusInternalServerError, err } - renderJSON(w, r, s) -} + return renderJSON(w, r, s) +}) diff --git a/http/static.go b/http/static.go index 10649085..17d1d9bb 100644 --- a/http/static.go +++ b/http/static.go @@ -1,23 +1,23 @@ package http +/* import ( "encoding/json" "log" - "net/http" "os" "path/filepath" "strings" - "text/template" - "github.com/GeertJohan/go.rice" "github.com/filebrowser/filebrowser/auth" + "github.com/filebrowser/filebrowser/storage" + "github.com/filebrowser/filebrowser/version" ) -func (e *env) getStaticData() map[string]interface{} { - e.RLockSettings() - defer e.RUnlockSettings() - - settings := e.GetSettings() +func getStaticData(storage *storage.Storage) (map[string]interface{}, error) { + settings, err := storage.Settings.Get() + if err != nil { + return nil, err + } staticURL := strings.TrimPrefix(settings.BaseURL+"/static", "/") @@ -25,7 +25,7 @@ func (e *env) getStaticData() map[string]interface{} { "Name": settings.Branding.Name, "DisableExternal": settings.Branding.DisableExternal, "BaseURL": settings.BaseURL, - "Version": lib.Version, + "Version": version.Version, "StaticURL": staticURL, "Signup": settings.Signup, "NoAuth": settings.AuthMethod == auth.MethodNoAuth, @@ -47,7 +47,12 @@ func (e *env) getStaticData() map[string]interface{} { } if settings.AuthMethod == auth.MethodJSONAuth { - auther := e.Auther.(*auth.JSONAuth) + raw, err := storage.Auth.Get() + if err != nil { + return nil, err + } + + auther := raw.(*auth.JSONAuth) if auther.ReCaptcha != nil { data["ReCaptcha"] = auther.ReCaptcha.Key != "" && auther.ReCaptcha.Secret != "" @@ -58,17 +63,21 @@ func (e *env) getStaticData() map[string]interface{} { b, _ := json.MarshalIndent(data, "", " ") data["Json"] = string(b) - - return data + return data, nil } -func (e *env) getStaticHandlers() (http.Handler, http.Handler) { +func getStaticHandlers(storage *storage.Storage) (http.Handler, http.Handler, error) { box := rice.MustFindBox("../frontend/dist") handler := http.FileServer(box.HTTPBox()) handleWithData := func(w http.ResponseWriter, r *http.Request, file string, contentType string) { w.Header().Set("Content-Type", contentType) index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(box.MustString(file))) + data, err := getStaticData(storage) + if err != nil { + + } + err := index.Execute(w, e.getStaticData()) if err != nil { @@ -115,3 +124,5 @@ func (e *env) getStaticHandlers() (http.Handler, http.Handler) { return index, static } + +*/ diff --git a/http/users.go b/http/users.go index aafcd899..2ce571ed 100644 --- a/http/users.go +++ b/http/users.go @@ -5,13 +5,17 @@ import ( "net/http" "sort" "strconv" - "strings" - + "github.com/filebrowser/filebrowser/errors" "github.com/filebrowser/filebrowser/users" "github.com/gorilla/mux" ) +type modifyUserRequest struct { + modifyRequest + Data *users.User `json:"data"` +} + func getUserID(r *http.Request) (uint, error) { vars := mux.Vars(r) i, err := strconv.ParseUint(vars["id"], 10, 0) @@ -21,117 +25,81 @@ func getUserID(r *http.Request) (uint, error) { return uint(i), err } -type modifyUserRequest struct { - modifyRequest - Data *users.User `json:"data"` -} - -func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, bool) { +func getUser(w http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) { if r.Body == nil { - httpErr(w, r, http.StatusBadRequest, nil) - return nil, false + return nil, errors.ErrEmptyRequest } req := &modifyUserRequest{} err := json.NewDecoder(r.Body).Decode(req) if err != nil { - httpErr(w, r, http.StatusBadRequest, err) - return nil, false + return nil, err } if req.What != "user" { - httpErr(w, r, http.StatusBadRequest, nil) - return nil, false + return nil, errors.ErrInvalidDataType } - return req, true + return req, nil } -func (e *env) usersGetHandler(w http.ResponseWriter, r *http.Request) { - user, ok := e.getUser(w, r) - if !ok { - return - } +func withSelfOrAdmin(fn handleFunc) handleFunc { + return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + id, err := getUserID(r) + if err != nil { + return http.StatusInternalServerError, err + } - if !user.Perm.Admin { - httpErr(w, r, http.StatusForbidden, nil) - return - } + if d.user.ID != id && !d.user.Perm.Admin { + return http.StatusForbidden, nil + } - users, err := e.GetUsers() + d.raw = id + return fn(w, r, d) + }) +} + +var usersGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + users, err := d.store.Users.Gets() if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } - + for _, u := range users { u.Password = "" } - + sort.Slice(users, func(i, j int) bool { return users[i].ID < users[j].ID }) + + return renderJSON(w, r, users) +}) - renderJSON(w, r, users) -} - -func (e *env) userSelfOrAdmin(w http.ResponseWriter, r *http.Request) (*users.User, uint, bool) { - user, ok := e.getUser(w, r) - if !ok { - return nil, 0, false +var userGetHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + u, err := d.store.Users.Get(d.raw.(uint)) + if err == errors.ErrNotExist { + return http.StatusNotFound, err } - - id, err := getUserID(r) + if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return nil, 0, false - } - - if user.ID != id && !user.Perm.Admin { - httpErr(w, r, http.StatusForbidden, nil) - return nil, 0, false - } - - return user, id, true -} - -func (e *env) userGetHandler(w http.ResponseWriter, r *http.Request) { - _, id, ok := e.userSelfOrAdmin(w, r) - if !ok { - return - } - - u, err := e.GetUser(id) - if err == lib.ErrNotExist { - httpErr(w, r, http.StatusNotFound, nil) - return - } - - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } u.Password = "" - renderJSON(w, r, u) -} + return renderJSON(w, r, u) +}) -func (e *env) userDeleteHandler(w http.ResponseWriter, r *http.Request) { - _, id, ok := e.userSelfOrAdmin(w, r) - if !ok { - return +var userDeleteHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + err := d.store.Users.Delete(d.raw.(uint)) + if err == errors.ErrNotExist { + return http.StatusNotFound, err } + + return http.StatusOK, nil +}) - err := e.DeleteUser(id) - if err == lib.ErrNotExist { - httpErr(w, r, http.StatusNotFound, nil) - return - } - - if err != nil { - httpErr(w, r, http.StatusInternalServerError, err) - } -} +/* func (e *env) userPostHandler(w http.ResponseWriter, r *http.Request) { _, ok := e.getAdminUser(w, r) @@ -237,4 +205,4 @@ func (e *env) userPutHandler(w http.ResponseWriter, r *http.Request) { if err != nil { httpErr(w, r, http.StatusInternalServerError, err) } -} +} */ diff --git a/http/utils.go b/http/utils.go new file mode 100644 index 00000000..1ea3c387 --- /dev/null +++ b/http/utils.go @@ -0,0 +1,37 @@ +package http + +import ( + "encoding/json" + "net/http" + "os" +) + +func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) { + marsh, err := json.Marshal(data) + + if err != nil { + return http.StatusInternalServerError, err + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + if _, err := w.Write(marsh); err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil +} + +func errToStatus(err error) int { + switch { + case err == nil: + return http.StatusOK + case os.IsPermission(err): + return http.StatusForbidden + case os.IsNotExist(err): + return http.StatusNotFound + case os.IsExist(err): + return http.StatusConflict + default: + return http.StatusInternalServerError + } +} diff --git a/http/websockets.go b/http/websockets.go index 7eef4780..28a3bbac 100644 --- a/http/websockets.go +++ b/http/websockets.go @@ -1,19 +1,6 @@ package http -import ( - "bufio" - "encoding/json" - "io" - "net/http" - "os" - "os/exec" - "strings" - - "github.com/filebrowser/filebrowser/search" - - "github.com/gorilla/websocket" -) - +/* var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -147,4 +134,4 @@ func (e *env) searchHandler(w http.ResponseWriter, r *http.Request) { httpErr(w, r, http.StatusInternalServerError, err) return } -} +} */ diff --git a/runner/parser.go b/runner/parser.go new file mode 100644 index 00000000..3477b98c --- /dev/null +++ b/runner/parser.go @@ -0,0 +1,34 @@ +package runner + +import ( + "os/exec" + + "github.com/mholt/caddy" + "github.com/filebrowser/filebrowser/settings" +) + +// ParseCommand parses the command taking in account if the current +// instance uses a shell to run the commands or just calls the binary +// directyly. +func ParseCommand(s *settings.Settings, raw string) ([]string, error) { + command := []string{} + + if len(s.Shell) == 0 { + cmd, args, err := caddy.SplitCommandAndArgs(raw) + if err != nil { + return nil, err + } + + _, err = exec.LookPath(cmd) + if err != nil { + return nil, err + } + + command = append(command, cmd) + command = append(command, args...) + } else { + command = append(s.Shell, raw) + } + + return command, nil +} diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 00000000..bb06c527 --- /dev/null +++ b/runner/runner.go @@ -0,0 +1,81 @@ +package runner + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/filebrowser/filebrowser/settings" + "github.com/filebrowser/filebrowser/users" +) + +// Runner is a commands runner. +type Runner struct { + *settings.Settings +} + +// RunHook runs the hooks for the before and after event. +func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error { + path = user.FullPath(path) + dst = user.FullPath(dst) + + if val, ok := r.Commands["before_"+evt]; ok { + for _, command := range val { + err := r.exec(command, "before_"+evt, path, dst, user) + if err != nil { + return err + } + } + } + + err := fn() + if err != nil { + return err + } + + if val, ok := r.Commands["after_"+evt]; ok { + for _, command := range val { + err := r.exec(command, "after_"+evt, path, dst, user) + if err != nil { + return err + } + } + } + + return nil +} + +func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error { + blocking := true + + if strings.HasSuffix(raw, "&") { + blocking = false + raw = strings.TrimSpace(strings.TrimSuffix(raw, "&")) + } + + command, err := ParseCommand(r.Settings, raw) + if err != nil { + return err + } + + cmd := exec.Command(command[0], command[1:]...) + cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) + cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) + cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt)) + cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) + cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst)) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if !blocking { + log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " ")) + return cmd.Start() + } + + log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " ")) + return cmd.Run() +}