package fbhttp import ( "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "net/http" "path/filepath" "sort" "strconv" "strings" "time" fberrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/share" "github.com/filebrowser/filebrowser/v2/users" "golang.org/x/crypto/bcrypt" ) func withPermShare(fn handleFunc) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Share || !d.user.Perm.Download { return http.StatusForbidden, nil } return fn(w, r, d) }) } var shareListHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var ( s []*share.Link err error ) if d.user.Perm.Admin { s, err = d.store.Share.All() } else { s, err = d.store.Share.FindByUserID(d.user.ID) } if errors.Is(err, fberrors.ErrNotExist) { return renderJSON(w, r, []*share.Link{}) } if err != nil { return http.StatusInternalServerError, err } sort.Slice(s, func(i, j int) bool { if s[i].UserID != s[j].UserID { return s[i].UserID < s[j].UserID } return s[i].Expire < s[j].Expire }) return renderJSON(w, r, s) }) var shareGetsHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { var ( s []*share.Link err error ) if d.user.Perm.Admin { s, err = getSharesForAdminPath(d, r.URL.Path) } else { s, err = d.store.Share.Gets(r.URL.Path, d.user.ID) } if errors.Is(err, fberrors.ErrNotExist) { return renderJSON(w, r, []*share.Link{}) } if err != nil { return http.StatusInternalServerError, err } return renderJSON(w, r, s) }) func getSharesForAdminPath(d *data, path string) ([]*share.Link, error) { links, err := d.store.Share.All() if err != nil { return nil, err } adminPath := filepath.Clean(d.user.FullPath(path)) owners := make(map[uint]*users.User) filtered := make([]*share.Link, 0, len(links)) for _, link := range links { owner, ok := owners[link.UserID] if !ok { owner, err = d.store.Users.Get(d.server.Root, d.server.FollowExternalSymlinks, link.UserID) if err != nil && !errors.Is(err, fberrors.ErrNotExist) { return nil, err } owners[link.UserID] = owner // owner is nil on ErrNotExist } if owner != nil && filepath.Clean(owner.FullPath(link.Path)) == adminPath { filtered = append(filtered, link) } } return filtered, nil } var shareDeleteHandler = withPermShare(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { hash := strings.TrimSuffix(r.URL.Path, "/") hash = strings.TrimPrefix(hash, "/") if hash == "" { return http.StatusBadRequest, nil } link, err := d.store.Share.GetByHash(hash) if err != nil { return errToStatus(err), err } if link.UserID != d.user.ID && !d.user.Perm.Admin { return http.StatusForbidden, nil } err = d.store.Share.Delete(hash) return errToStatus(err), err }) var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { // Only allow sharing paths that currently exist. Otherwise a share could be // created for a non-existent path and would silently start exposing // whatever file later appears there. // // d.user.Fs is scoped, so Stat also refuses to follow a symlink whose target // escapes the user's scope: that returns a permission error here and so // blocks creating a share that points out of scope. if _, err := d.user.Fs.Stat(r.URL.Path); err != nil { return errToStatus(err), err } var s *share.Link var body share.CreateBody if r.Body != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { return http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err) } defer r.Body.Close() } bytes := make([]byte, 6) _, err := rand.Read(bytes) if err != nil { return http.StatusInternalServerError, err } str := base64.URLEncoding.EncodeToString(bytes) var expire int64 = 0 if body.Expires != "" { num, err := strconv.Atoi(body.Expires) if err != nil { return http.StatusInternalServerError, err } var add time.Duration switch body.Unit { case "seconds": add = time.Second * time.Duration(num) case "minutes": add = time.Minute * time.Duration(num) case "days": add = time.Hour * 24 * time.Duration(num) default: add = time.Hour * time.Duration(num) } expire = time.Now().Add(add).Unix() } hash, status, err := getSharePasswordHash(body) if err != nil { return status, err } var token string if len(hash) > 0 { tokenBuffer := make([]byte, 96) if _, err := rand.Read(tokenBuffer); err != nil { return http.StatusInternalServerError, err } token = base64.URLEncoding.EncodeToString(tokenBuffer) } s = &share.Link{ Path: r.URL.Path, Hash: str, Expire: expire, UserID: d.user.ID, PasswordHash: string(hash), Token: token, } if err := d.store.Share.Save(s); err != nil { return http.StatusInternalServerError, err } return renderJSON(w, r, s) }) func getSharePasswordHash(body share.CreateBody) (data []byte, statuscode int, err error) { if body.Password == "" { return nil, 0, nil } hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost) if err != nil { return nil, http.StatusInternalServerError, fmt.Errorf("failed to hash password: %w", err) } return hash, 0, nil }