package http import ( "context" "errors" "fmt" "io" "math" "net/http" "net/url" "os" "path" "path/filepath" "strconv" "strings" "github.com/mholt/archiver/v3" "github.com/shirou/gopsutil/v3/disk" "github.com/spf13/afero" fbErrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/fileutils" ) var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { file, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: r.URL.Path, Modify: d.user.Perm.Modify, Expand: true, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, Content: true, }) // if the path does not exist and its the trash dir - create it if os.IsNotExist(err) && d.user.TrashDir != "" { if d.user.FullPath(r.URL.Path) == d.user.FullPath(d.user.TrashDir) { err = d.user.Fs.MkdirAll(d.user.TrashDir, 0775) //nolint:gomnd if err != nil { return errToStatus(err), err } file, err = files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: r.URL.Path, Modify: d.user.Perm.Modify, Expand: true, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, Content: true, }) } } if err != nil { return errToStatus(err), err } if file.IsSymlink && symlinkOutOfScope(d, file) { return errToStatus(fbErrors.ErrNotExist), fbErrors.ErrNotExist } if r.URL.Query().Get("disk_usage") == "true" { du, inodes, err := fileutils.DiskUsage(file.Fs, file.Path) if err != nil { return http.StatusInternalServerError, err } file.DiskUsage = du file.Inodes = inodes file.Content = "" return renderJSON(w, r, file) } if file.IsDir { file.Listing.Sorting = d.user.Sorting file.Listing.ApplySort() file.Listing.FilterItems(func(fi *files.FileInfo) bool { // remove files that should be hidden _, exists := d.server.HiddenFiles[fi.Name] if exists { return false } // remove symlinks that link outside base path if fi.IsSymlink { link := fi.Link isAbs := filepath.IsAbs(link) if !isAbs { link = filepath.Join(d.user.FullPath(file.Path), link) } link = filepath.Clean(link) if !strings.HasPrefix(link, d.server.Root) { return false } if isAbs { fi.Link = strings.TrimPrefix(link, d.server.Root) } } return true }) return renderJSON(w, r, file) } if checksum := r.URL.Query().Get("checksum"); checksum != "" { err := file.Checksum(checksum) if errors.Is(err, fbErrors.ErrInvalidOption) { return http.StatusBadRequest, nil } else if err != nil { return http.StatusInternalServerError, err } // do not waste bandwidth if we just want the checksum file.Content = "" } return renderJSON(w, r, file) }) func resourceDeleteHandler(fileCache FileCache) handleFunc { return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { if r.URL.Path == "/" || !d.user.Perm.Delete { return http.StatusForbidden, nil } file, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: r.URL.Path, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, }) if err != nil { return errToStatus(err), err } // delete thumbnails err = delThumbs(r.Context(), fileCache, file) if err != nil { return errToStatus(err), err } skipTrash := r.URL.Query().Get("skip_trash") == "true" if d.user.TrashDir == "" || skipTrash { err = d.RunHook(func() error { return d.user.Fs.RemoveAll(r.URL.Path) }, "delete", r.URL.Path, "", d.user) } else { src := r.URL.Path dst := d.user.TrashDir if !d.Check(src) || !d.Check(dst) { return http.StatusForbidden, nil } src = path.Clean("/" + src) dst = path.Clean("/" + dst) err = d.user.Fs.MkdirAll(dst, 0775) //nolint:gomnd if err != nil { return errToStatus(err), err } dst = path.Join(dst, file.Name) err = fileutils.MoveFile(d.user.Fs, src, dst) } if err != nil { return errToStatus(err), err } return http.StatusNoContent, nil }) } func resourcePostHandler(fileCache FileCache) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } // Directories creation on POST. if strings.HasSuffix(r.URL.Path, "/") { err := d.user.Fs.MkdirAll(r.URL.Path, files.PermDir) return errToStatus(err), err } // Archive creation on POST. if strings.HasSuffix(r.URL.Path, "/archive") { if !d.user.Perm.Create { return http.StatusForbidden, nil } return archiveHandler(r, d) } file, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: r.URL.Path, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: d.server.TypeDetectionByHeader, Checker: d, }) if err == nil { if r.URL.Query().Get("override") != "true" { return http.StatusConflict, nil } // Permission for overwriting the file if !d.user.Perm.Modify { return http.StatusForbidden, nil } err = delThumbs(r.Context(), fileCache, file) if err != nil { return errToStatus(err), err } } err = d.RunHook(func() error { info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body) if writeErr != nil { return writeErr } etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size()) w.Header().Set("ETag", etag) return nil }, "upload", r.URL.Path, "", d.user) if err != nil { _ = d.user.Fs.RemoveAll(r.URL.Path) } return errToStatus(err), err }) } var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Modify || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } // Only allow PUT for files. if strings.HasSuffix(r.URL.Path, "/") { return http.StatusMethodNotAllowed, nil } exists, err := afero.Exists(d.user.Fs, r.URL.Path) if err != nil { return http.StatusInternalServerError, err } if !exists { return http.StatusNotFound, nil } err = d.RunHook(func() error { info, writeErr := writeFile(d.user.Fs, r.URL.Path, r.Body) if writeErr != nil { return writeErr } etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size()) w.Header().Set("ETag", etag) return nil }, "save", r.URL.Path, "", d.user) return errToStatus(err), err }) func checkSrcDstAccess(src, dst string, d *data) error { if !d.Check(src) || !d.Check(dst) { return fbErrors.ErrPermissionDenied } if dst == "/" || src == "/" { return fbErrors.ErrPermissionDenied } if err := checkParent(src, dst); err != nil { return fbErrors.ErrInvalidRequestParams } return nil } func resourcePatchHandler(fileCache FileCache) handleFunc { return withUser(func(_ 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") if action == "chmod" { err := chmodActionHandler(r, d) return errToStatus(err), err } dst, err := url.QueryUnescape(dst) if err != nil { return errToStatus(err), err } err = checkSrcDstAccess(src, dst, d) if err != nil { return errToStatus(err), err } override := r.URL.Query().Get("override") == "true" rename := r.URL.Query().Get("rename") == "true" unarchive := action == "unarchive" if !override && !rename && !unarchive { if _, err = d.user.Fs.Stat(dst); err == nil { return http.StatusConflict, nil } } if rename { dst = addVersionSuffix(dst, d.user.Fs) } // Permission for overwriting the file if override && !d.user.Perm.Modify { return http.StatusForbidden, nil } err = d.RunHook(func() error { if unarchive { return unarchiveAction(src, dst, d, override) } return patchAction(r.Context(), action, src, dst, d, fileCache) }, action, src, dst, d.user) return errToStatus(err), err }) } func checkParent(src, dst string) error { rel, err := filepath.Rel(src, dst) if err != nil { return err } rel = filepath.ToSlash(rel) if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." { return fbErrors.ErrSourceIsParent } return nil } // Checks if path contains symlink to out-of-scope targets. // Returns error ErrNotExist if it does. func symlinkOutOfScope(d *data, file *files.FileInfo) bool { var err error link := "" if lsf, ok := d.user.Fs.(afero.LinkReader); ok { if link, err = lsf.ReadlinkIfPossible(file.Path); err != nil { return false } } if !filepath.IsAbs(link) { link = filepath.Join(d.user.FullPath(file.Path), link) } link = filepath.Clean(link) return !strings.HasPrefix(link, d.server.Root) } func addVersionSuffix(source string, fs afero.Fs) string { counter := 1 dir, name := path.Split(source) ext := filepath.Ext(name) base := strings.TrimSuffix(name, ext) for { if _, err := fs.Stat(source); err != nil { break } renamed := fmt.Sprintf("%s(%d)%s", base, counter, ext) source = path.Join(dir, renamed) counter++ } return source } func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) { dir, _ := path.Split(dst) err := fs.MkdirAll(dir, files.PermDir) if err != nil { return nil, err } file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile) if err != nil { return nil, err } defer file.Close() _, err = io.Copy(file, in) if err != nil { return nil, err } // Gets the info about the file. info, err := file.Stat() if err != nil { return nil, err } return info, nil } func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error { for _, previewSizeName := range PreviewSizeNames() { size, _ := ParsePreviewSize(previewSizeName) if err := fileCache.Delete(ctx, previewCacheKey(file, size)); err != nil { return err } } return nil } func patchAction(ctx context.Context, action, src, dst string, d *data, fileCache FileCache) error { switch action { case "copy": if !d.user.Perm.Create { return fbErrors.ErrPermissionDenied } return fileutils.CopyScoped(d.user.Fs, src, dst, d.server.Root) case "rename": if !d.user.Perm.Rename { return fbErrors.ErrPermissionDenied } src = path.Clean("/" + src) dst = path.Clean("/" + dst) file, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: src, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: false, Checker: d, }) if err != nil { return err } // delete thumbnails err = delThumbs(ctx, fileCache, file) if err != nil { return err } return fileutils.MoveFile(d.user.Fs, src, dst) default: return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams) } } type DiskUsageResponse struct { Total uint64 `json:"total"` Used uint64 `json:"used"` } //lint:ignore U1000 unused in this fork //nolint:deadcode var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { file, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: r.URL.Path, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: false, Checker: d, Content: false, }) if err != nil { return errToStatus(err), err } fPath := file.RealPath() if !file.IsDir { return renderJSON(w, r, &DiskUsageResponse{ Total: 0, Used: 0, }) } usage, err := disk.UsageWithContext(r.Context(), fPath) if err != nil { return errToStatus(err), err } return renderJSON(w, r, &DiskUsageResponse{ Total: usage.Total, Used: usage.Used, }) }) func unarchiveAction(src, dst string, d *data, overwrite bool) error { if !d.user.Perm.Create { return fbErrors.ErrPermissionDenied } src = d.user.FullPath(path.Clean("/" + src)) dst = d.user.FullPath(path.Clean("/" + dst)) arch, err := archiver.ByExtension(src) if err != nil { return err } switch a := arch.(type) { case *archiver.Rar: a.OverwriteExisting = overwrite case *archiver.Tar: a.OverwriteExisting = overwrite case *archiver.TarBz2: a.OverwriteExisting = overwrite case *archiver.TarGz: a.OverwriteExisting = overwrite case *archiver.TarLz4: a.OverwriteExisting = overwrite case *archiver.TarSz: a.OverwriteExisting = overwrite case *archiver.TarXz: a.OverwriteExisting = overwrite case *archiver.Zip: a.OverwriteExisting = overwrite } unarchiver, ok := arch.(archiver.Unarchiver) if ok { if err := unarchiver.Unarchive(src, dst); err != nil { return fbErrors.ErrInvalidRequestParams } return nil } decompressor, ok := arch.(archiver.Decompressor) if ok { return fileutils.Decompress(src, dst, decompressor) } return fbErrors.ErrInvalidRequestParams } func archiveHandler(r *http.Request, d *data) (int, error) { dir := strings.TrimSuffix(r.URL.Path, "/archive") destDir, err := files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: dir, Modify: d.user.Perm.Modify, Expand: false, ReadHeader: false, Checker: d, }) if err != nil { return errToStatus(err), err } filenames, err := parseQueryFiles(r, destDir, d.user) if err != nil { return http.StatusInternalServerError, err } archFile, err := parseQueryFilename(r, destDir) if err != nil { return http.StatusUnprocessableEntity, fbErrors.NewHTTPError(err, "validation.emptyName") } extension, ar, err := parseArchiver(r.URL.Query().Get("algo")) if err != nil { return http.StatusInternalServerError, err } archFile += extension _, err = d.user.Fs.Stat(archFile) if err == nil { return http.StatusUnprocessableEntity, fbErrors.NewHTTPError(err, "resource.alreadyExists") } dir, _ = path.Split(archFile) err = d.user.Fs.MkdirAll(dir, 0775) //nolint:gomnd if err != nil { return errToStatus(err), err } for i, path := range filenames { _, err = d.user.Fs.Stat(path) if err != nil { return errToStatus(err), err } filenames[i] = d.user.FullPath(path) } dst := d.user.FullPath(archFile) err = ar.Archive(filenames, dst) if err != nil { return http.StatusInternalServerError, err } return errToStatus(err), err } func parseQueryFilename(r *http.Request, f *files.FileInfo) (string, error) { name := r.URL.Query().Get("name") name, err := url.QueryUnescape(strings.Replace(name, "+", "%2B", -1)) if err != nil { return "", err } name = strings.TrimSpace(name) if name == "" { return "", fmt.Errorf("empty name provided") } return filepath.Join(f.Path, slashClean(name)), nil } func parseArchiver(algo string) (string, archiver.Archiver, error) { switch algo { case "zip", "true", "": return ".zip", archiver.NewZip(), nil case "tar": return ".tar", archiver.NewTar(), nil case "targz": return ".tar.gz", archiver.NewTarGz(), nil case "tarbz2": return ".tar.bz2", archiver.NewTarBz2(), nil case "tarxz": return ".tar.xz", archiver.NewTarXz(), nil case "tarlz4": return ".tar.lz4", archiver.NewTarLz4(), nil case "tarsz": return ".tar.sz", archiver.NewTarSz(), nil default: return "", nil, fmt.Errorf("format not implemented") } } func chmodActionHandler(r *http.Request, d *data) error { target := r.URL.Path perms := r.URL.Query().Get("permissions") recursive := r.URL.Query().Get("recursive") == "true" recursionType := r.URL.Query().Get("type") if !d.user.Perm.Modify { return fbErrors.ErrPermissionDenied } if !d.Check(target) || target == "/" { return fbErrors.ErrPermissionDenied } mode, err := strconv.ParseUint(perms, 10, 32) if err != nil { return fbErrors.ErrInvalidRequestParams } permMode := normalizeFileMode(mode) info, err := d.user.Fs.Stat(target) if err != nil { return err } if recursive && info.IsDir() { var recFilter func(i os.FileInfo) bool switch recursionType { case "directories": recFilter = func(i os.FileInfo) bool { return i.IsDir() } case "files": recFilter = func(i os.FileInfo) bool { return !i.IsDir() } default: recFilter = func(_ os.FileInfo) bool { return true } } return afero.Walk(d.user.Fs, target, func(name string, info os.FileInfo, err error) error { if err == nil { if recFilter(info) { err = d.user.Fs.Chmod(name, os.FileMode(permMode)) } } return err }) } return d.user.Fs.Chmod(target, os.FileMode(permMode)) } func normalizeFileMode(m uint64) uint32 { fullPerms := 511 if m > math.MaxInt32 { return uint32(fullPerms) } return uint32(m) }