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
This commit is contained in:
@@ -3,6 +3,8 @@ package http
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
@@ -10,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/filebrowser/filebrowser/v2/errors"
|
||||
"github.com/filebrowser/filebrowser/v2/share"
|
||||
)
|
||||
@@ -79,10 +83,15 @@ var shareDeleteHandler = withPermShare(func(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||
var s *share.Link
|
||||
rawExpire := r.URL.Query().Get("expires")
|
||||
unit := r.URL.Query().Get("unit")
|
||||
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 rawExpire == "" {
|
||||
if body.Expires == "" {
|
||||
var err error
|
||||
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
|
||||
if err == nil {
|
||||
@@ -103,14 +112,15 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
|
||||
var expire int64 = 0
|
||||
|
||||
if rawExpire != "" {
|
||||
num, err := strconv.Atoi(rawExpire)
|
||||
if body.Expires != "" {
|
||||
//nolint:govet
|
||||
num, err := strconv.Atoi(body.Expires)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
var add time.Duration
|
||||
switch unit {
|
||||
switch body.Unit {
|
||||
case "seconds":
|
||||
add = time.Second * time.Duration(num)
|
||||
case "minutes":
|
||||
@@ -124,11 +134,27 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
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,
|
||||
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 {
|
||||
@@ -137,3 +163,16 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user