diff --git a/caddy/filemanager/filemanager.go b/caddy/filemanager/filemanager.go index 30481871..35d5e06d 100644 --- a/caddy/filemanager/filemanager.go +++ b/caddy/filemanager/filemanager.go @@ -12,9 +12,8 @@ import ( "path/filepath" "strings" - "golang.org/x/net/webdav" - . "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/dir" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -137,7 +136,7 @@ func parse(c *caddy.Controller) ([]*config, error) { Regexp: &Regexp{Raw: "\\/\\..+"}, }}, CSS: "", - FileSystem: webdav.Dir(baseScope), + FileSystem: dir.Dir(baseScope), }) if err != nil { diff --git a/caddy/hugo/setup.go b/caddy/hugo/setup.go index 8030773c..aa72ed78 100644 --- a/caddy/hugo/setup.go +++ b/caddy/hugo/setup.go @@ -12,9 +12,9 @@ import ( "strings" "github.com/hacdias/filemanager" + "github.com/hacdias/filemanager/dir" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" - "golang.org/x/net/webdav" ) var ( @@ -112,7 +112,7 @@ func parse(c *caddy.Controller) ([]*filemanager.FileManager, error) { Regexp: &filemanager.Regexp{Raw: "\\/\\..+"}, }}, CSS: "", - FileSystem: webdav.Dir(directory), + FileSystem: dir.Dir(directory), }) if err != nil { diff --git a/cmd/filemanager/main.go b/cmd/filemanager/main.go index 2f141fdf..a85561c2 100644 --- a/cmd/filemanager/main.go +++ b/cmd/filemanager/main.go @@ -11,8 +11,7 @@ import ( "strings" "github.com/hacdias/filemanager" - - "golang.org/x/net/webdav" + "github.com/hacdias/filemanager/dir" ) // confFile contains the configuration file for this File Manager instance. @@ -65,7 +64,7 @@ func main() { Commands: strings.Split(strings.TrimSpace(commands), " "), Rules: []*filemanager.Rule{}, CSS: "", - FileSystem: webdav.Dir(scope), + FileSystem: dir.Dir(scope), }) if err != nil { diff --git a/dir/dir.go b/dir/dir.go new file mode 100644 index 00000000..d86dbe48 --- /dev/null +++ b/dir/dir.go @@ -0,0 +1,226 @@ +// Package dir implements a FileSystem interface using the native +// file system restricted to a specific directory tree. Originally from +// https://github.com/golang/net/blob/master/webdav/file.go#L68 +package dir + +import ( + "errors" + "io" + "os" + "path" + "path/filepath" + "strings" +) + +// A Dir uses the native file system restricted to a specific directory tree. +// +// While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's +// string value is a filename on the native file system, not a URL, so it is +// separated by filepath.Separator, which isn't necessarily '/'. +// +// An empty Dir is treated as ".". +type Dir string + +func (d Dir) resolve(name string) string { + // This implementation is based on Dir.Open's code in the standard net/http package. + if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 || + strings.Contains(name, "\x00") { + return "" + } + + dir := string(d) + if dir == "" { + dir = "." + } + + return filepath.Join(dir, filepath.FromSlash(SlashClean(name))) +} + +// Mkdir implements os.Mkdir in this directory context. +func (d Dir) Mkdir(name string, perm os.FileMode) error { + if name = d.resolve(name); name == "" { + return os.ErrNotExist + } + return os.Mkdir(name, perm) +} + +// OpenFile implements os.OpenFile in this directory context. +func (d Dir) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { + if name = d.resolve(name); name == "" { + return nil, os.ErrNotExist + } + f, err := os.OpenFile(name, flag, perm) + if err != nil { + return nil, err + } + return f, nil +} + +// RemoveAll implements os.RemoveAll in this directory context. +func (d Dir) RemoveAll(name string) error { + if name = d.resolve(name); name == "" { + return os.ErrNotExist + } + + if name == filepath.Clean(string(d)) { + // Prohibit removing the virtual root directory. + return os.ErrInvalid + } + return os.RemoveAll(name) +} + +// Rename implements os.Rename in this directory context. +func (d Dir) Rename(oldName, newName string) error { + if oldName = d.resolve(oldName); oldName == "" { + return os.ErrNotExist + } + if newName = d.resolve(newName); newName == "" { + return os.ErrNotExist + } + if root := filepath.Clean(string(d)); root == oldName || root == newName { + // Prohibit renaming from or to the virtual root directory. + return os.ErrInvalid + } + return os.Rename(oldName, newName) +} + +// Stat implements os.Stat in this directory context. +func (d Dir) Stat(name string) (os.FileInfo, error) { + if name = d.resolve(name); name == "" { + return nil, os.ErrNotExist + } + + return os.Stat(name) +} + +// Copy copies a file or directory from src to dst. If it is +// a directory, all of the files and sub-directories will be copied. +func (d Dir) Copy(src, dst string) error { + if src = d.resolve(src); src == "" { + return os.ErrNotExist + } + + if dst = d.resolve(dst); dst == "" { + return os.ErrNotExist + } + + if root := filepath.Clean(string(d)); root == src || root == dst { + // Prohibit copying from or to the virtual root directory. + return os.ErrInvalid + } + + info, err := d.Stat(src) + if err != nil { + return err + } + + if info.IsDir() { + return CopyDir(src, dst) + } + + return CopyFile(src, dst) +} + +// SlashClean is equivalent to but slightly more efficient than +// path.Clean("/" + name). +func SlashClean(name string) string { + if name == "" || name[0] != '/' { + name = "/" + name + } + return path.Clean(name) +} + +// CopyFile copies a file from source to dest and returns +// an error if any. +func CopyFile(source string, dest string) error { + // Open the source file. + src, err := os.Open(source) + if err != nil { + return err + } + defer src.Close() + + // Makes the directory needed to create the dst + // file. + err = os.MkdirAll(filepath.Dir(dest), 0666) + if err != nil { + return err + } + + // Create the destination file. + dst, err := os.Create(dest) + if err != nil { + return err + } + defer dst.Close() + + // Copy the contents of the file. + _, err = io.Copy(dst, src) + if err != nil { + return err + } + + // Copy the mode if the user can't + // open the file. + info, err := os.Stat(source) + if err != nil { + err = os.Chmod(dest, info.Mode()) + if err != nil { + return err + } + } + + return nil +} + +// CopyDir copies a directory from source to dest and all +// of its sub-directories. It doesn't stop if it finds an error +// during the copy. Returns an error if any. +func CopyDir(source string, dest string) error { + // Get properties of source. + srcinfo, err := os.Stat(source) + if err != nil { + return err + } + + // Create the destination directory. + err = os.MkdirAll(dest, srcinfo.Mode()) + if err != nil { + return err + } + + dir, _ := os.Open(source) + obs, err := dir.Readdir(-1) + + var errs []error + + for _, obj := range obs { + fsource := source + "/" + obj.Name() + fdest := dest + "/" + obj.Name() + + if obj.IsDir() { + // Create sub-directories, recursively. + err = CopyDir(fsource, fdest) + if err != nil { + errs = append(errs, err) + } + } else { + // Perform the file copy. + err = CopyFile(fsource, fdest) + if err != nil { + errs = append(errs, err) + } + } + } + + var errString string + for _, err := range errs { + errString += err.Error() + "\n" + } + + if errString != "" { + return errors.New(errString) + } + + return nil +} diff --git a/file.go b/file.go index 4cf178cf..7534c531 100644 --- a/file.go +++ b/file.go @@ -2,14 +2,12 @@ package filemanager import ( "bytes" - "context" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/hex" "errors" - "fmt" "hash" "io" "io/ioutil" @@ -89,7 +87,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { Path: filepath.Join(string(u.FileSystem), url.Path), } - info, err := u.FileSystem.Stat(context.TODO(), url.Path) + info, err := u.FileSystem.Stat(url.Path) if err != nil { return i, err } @@ -112,7 +110,7 @@ func getInfo(url *url.URL, c *FileManager, u *User) (*file, error) { func (i *file) getListing(c *RequestContext, r *http.Request) error { // Gets the directory information using the Virtual File System of // the user configuration. - f, err := c.User.FileSystem.OpenFile(context.TODO(), c.FI.VirtualPath, os.O_RDONLY, 0) + f, err := c.User.FileSystem.OpenFile(c.FI.VirtualPath, os.O_RDONLY, 0) if err != nil { return err } @@ -446,71 +444,3 @@ func editorLanguage(mode string) string { return mode } - -func copyFile(source string, dest string) (err error) { - sourcefile, err := os.Open(source) - if err != nil { - return err - } - - defer sourcefile.Close() - - destfile, err := os.Create(dest) - if err != nil { - return err - } - - defer destfile.Close() - - _, err = io.Copy(destfile, sourcefile) - if err == nil { - sourceinfo, err := os.Stat(source) - if err != nil { - err = os.Chmod(dest, sourceinfo.Mode()) - if err != nil { - return err - } - } - - } - - return -} - -func copyDir(source string, dest string) (err error) { - // get properties of source dir - sourceinfo, err := os.Stat(source) - if err != nil { - return err - } - - // create dest dir - err = os.MkdirAll(dest, sourceinfo.Mode()) - if err != nil { - return err - } - - directory, _ := os.Open(source) - objects, err := directory.Readdir(-1) - - for _, obj := range objects { - sourcefilepointer := source + "/" + obj.Name() - destinationfilepointer := dest + "/" + obj.Name() - - if obj.IsDir() { - // create sub-directories - recursively - err = copyDir(sourcefilepointer, destinationfilepointer) - if err != nil { - fmt.Println(err) - } - } else { - // perform copy - err = copyFile(sourcefilepointer, destinationfilepointer) - if err != nil { - fmt.Println(err) - } - } - } - - return -} diff --git a/filemanager.go b/filemanager.go index 38cd3f62..b337f1a4 100644 --- a/filemanager.go +++ b/filemanager.go @@ -11,8 +11,8 @@ import ( rice "github.com/GeertJohan/go.rice" "github.com/asdine/storm" + "github.com/hacdias/filemanager/dir" "github.com/mholt/caddy" - "golang.org/x/net/webdav" ) var ( @@ -74,7 +74,7 @@ type User struct { Admin bool `json:"admin"` // FileSystem is the virtual file system the user has access. - FileSystem webdav.Dir `json:"filesystem"` + FileSystem dir.Dir `json:"filesystem"` // Rules is an array of access and deny rules. Rules []*Rule `json:"rules"` @@ -134,7 +134,7 @@ var DefaultUser = User{ Rules: []*Rule{}, CSS: "", Admin: true, - FileSystem: webdav.Dir("."), + FileSystem: dir.Dir("."), } // New creates a new File Manager instance. If 'database' file already diff --git a/http.go b/http.go index 1e12e2d0..b534d421 100644 --- a/http.go +++ b/http.go @@ -142,6 +142,8 @@ func apiHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, code, err = commandsHandler(c, w, r) case "plugins": code, err = pluginsHandler(c, w, r) + default: + code = http.StatusNotFound } if code >= 300 || err != nil { diff --git a/resource.go b/resource.go index 34a673ac..b3b5c749 100644 --- a/resource.go +++ b/resource.go @@ -1,7 +1,6 @@ package filemanager import ( - "context" "errors" "fmt" "io" @@ -10,15 +9,16 @@ import ( "os" "path/filepath" "strings" + + "github.com/hacdias/filemanager/dir" ) -// DISCLAIMER: this doesn't sanitize the target path so some may think -// that path trasversal would be possible and the user could change files -// outside of their scope. The User.FileSystem variable is of type webdav.Dir -// which does those checks so this package doesn't need to do them. -// https://github.com/golang/net/blob/master/webdav/file.go#L68 - func resourceHandler(c *RequestContext, w http.ResponseWriter, r *http.Request) (int, error) { + r.URL.Path = dir.SlashClean(r.URL.Path) + if !c.User.Allowed(r.URL.Path) { + return http.StatusForbidden, nil + } + switch r.Method { case http.MethodGet: return resourceGetHandler(c, w, r) @@ -131,7 +131,7 @@ func resourceDeleteHandler(c *RequestContext, w http.ResponseWriter, r *http.Req } // Remove the file or folder. - err := c.User.FileSystem.RemoveAll(context.TODO(), r.URL.Path) + err := c.User.FileSystem.RemoveAll(r.URL.Path) if err != nil { return errorToHTTP(err, true), err } @@ -157,7 +157,7 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re } // Otherwise we try to create the directory. - err := c.User.FileSystem.Mkdir(context.TODO(), r.URL.Path, 0666) + err := c.User.FileSystem.Mkdir(r.URL.Path, 0666) return errorToHTTP(err, false), err } @@ -165,13 +165,13 @@ func resourcePostPutHandler(c *RequestContext, w http.ResponseWriter, r *http.Re // desirable to override 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 { - if _, err := c.User.FileSystem.Stat(context.TODO(), r.URL.Path); err == nil { + if _, err := c.User.FileSystem.Stat(r.URL.Path); err == nil { return http.StatusConflict, errors.New("There is already a file on that path") } } // Create/Open the file. - f, err := c.User.FileSystem.OpenFile(context.TODO(), r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + f, err := c.User.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { return errorToHTTP(err, false), err } @@ -202,6 +202,7 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ } dst := r.Header.Get("Destination") + action := r.Header.Get("Action") dst, err := url.QueryUnescape(dst) if err != nil { return errorToHTTP(err, true), err @@ -213,7 +214,12 @@ func resourcePatchHandler(c *RequestContext, w http.ResponseWriter, r *http.Requ return http.StatusForbidden, nil } - err = c.User.FileSystem.Rename(context.TODO(), src, dst) + if action == "copy" { + err = c.User.FileSystem.Copy(src, dst) + } else { + err = c.User.FileSystem.Rename(src, dst) + } + return errorToHTTP(err, true), err }