filebrowser/http/share.go
Alvaro Aleman d8f415f8ab
feat: allow to password protect shares (#1252)
This changes allows to password protect shares. It works by:
* Allowing to optionally pass a password when creating a share
* If set, the password + salt that is configured via a new flag will be
  hashed via bcrypt and the hash stored together with the rest of the
  share
* Additionally, a random 96 byte long token gets generated and stored
  as part of the share
* When the backend retrieves an unauthenticated request for a share that
  has authentication configured, it will return a http 401
* The frontend detects this and will show a login prompt
* The actual download links are protected via an url arg that contains
  the previously generated token. This allows us to avoid buffering the
  download in the browser and allows pasting the link without breaking
  it
2021-03-02 12:00:18 +01:00

179 lines
3.9 KiB
Go

package http
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"path"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/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
}
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 err == errors.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) {
s, err := d.store.Share.Gets(r.URL.Path, d.user.ID)
if err == errors.ErrNotExist {
return renderJSON(w, r, []*share.Link{})
}
if err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, s)
})
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 http.StatusBadRequest, 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) {
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()
}
if body.Expires == "" {
var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
if err == nil {
if _, err := w.Write([]byte(path.Join(d.server.BaseURL, "/share/", s.Hash))); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
}
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 != "" {
//nolint:govet
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
}