2019-01-05 22:44:33 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
2021-03-02 11:00:18 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2019-01-05 22:44:33 +00:00
|
|
|
"net/http"
|
2020-12-24 18:02:28 +00:00
|
|
|
"sort"
|
2019-01-05 22:44:33 +00:00
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
2021-03-02 11:00:18 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
2024-12-17 00:01:55 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/share"
|
2019-01-05 22:44:33 +00:00
|
|
|
)
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// shareListHandler returns a list of all share links.
|
|
|
|
// @Summary List share links
|
|
|
|
// @Description Returns a list of share links for the current user, or all links if the user is an admin.
|
|
|
|
// @Tags Shares
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Success 200 {array} share.Link "List of share links"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
|
|
// @Router /api/shares [get]
|
|
|
|
func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
2020-12-24 18:02:28 +00:00
|
|
|
var (
|
|
|
|
s []*share.Link
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
if d.user.Perm.Admin {
|
2024-11-21 00:15:30 +00:00
|
|
|
s, err = store.Share.All()
|
2020-12-24 18:02:28 +00:00
|
|
|
} else {
|
2024-11-21 00:15:30 +00:00
|
|
|
s, err = store.Share.FindByUserID(d.user.ID)
|
2020-12-24 18:02:28 +00:00
|
|
|
}
|
|
|
|
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)
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2020-12-24 18:02:28 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// shareGetsHandler retrieves share links for a specific resource path.
|
|
|
|
// @Summary Get share links by path
|
|
|
|
// @Description Retrieves all share links associated with a specific resource path for the current user.
|
|
|
|
// @Tags Shares
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param path query string true "Resource path for which to retrieve share links"
|
|
|
|
// @Success 200 {array} share.Link "List of share links for the specified path"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
|
|
// @Router /api/share [get]
|
|
|
|
func shareGetsHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
path := r.URL.Query().Get("path")
|
|
|
|
s, err := store.Share.Gets(path, d.user.ID)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err == errors.ErrNotExist {
|
|
|
|
return renderJSON(w, r, []*share.Link{})
|
|
|
|
}
|
2025-01-05 19:05:33 +00:00
|
|
|
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
2024-11-26 17:21:41 +00:00
|
|
|
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
return renderJSON(w, r, s)
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// shareDeleteHandler deletes a specific share link by its hash.
|
|
|
|
// @Summary Delete a share link
|
|
|
|
// @Description Deletes a share link specified by its hash.
|
|
|
|
// @Tags Shares
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param hash path string true "Hash of the share link to delete"
|
|
|
|
// @Success 200 "Share link deleted successfully"
|
|
|
|
// @Failure 400 {object} map[string]string "Bad request - missing or invalid hash"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
|
|
// @Router /api/shares/{hash} [delete]
|
|
|
|
func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
hash := r.URL.Query().Get("hash")
|
2019-01-05 22:44:33 +00:00
|
|
|
|
|
|
|
if hash == "" {
|
|
|
|
return http.StatusBadRequest, nil
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
err := store.Share.Delete(hash)
|
2024-02-10 00:13:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return errToStatus(err), err
|
|
|
|
}
|
|
|
|
|
2019-01-05 22:44:33 +00:00
|
|
|
return errToStatus(err), err
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2024-02-10 00:13:02 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// sharePostHandler creates a new share link.
|
|
|
|
// @Summary Create a share link
|
|
|
|
// @Description Creates a new share link with an optional expiration time and password protection.
|
|
|
|
// @Tags Shares
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Param body body share.CreateBody true "Share link creation parameters"
|
|
|
|
// @Success 200 {object} share.Link "Created share link"
|
|
|
|
// @Failure 400 {object} map[string]string "Bad request - failed to decode body"
|
|
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
|
|
// @Router /api/shares [post]
|
|
|
|
func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
2019-01-05 22:44:33 +00:00
|
|
|
var s *share.Link
|
2021-03-02 11:00:18 +00:00
|
|
|
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()
|
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
secure_hash, err := generateShortUUID()
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var expire int64 = 0
|
|
|
|
|
2021-03-02 11:00:18 +00:00
|
|
|
if body.Expires != "" {
|
|
|
|
//nolint:govet
|
|
|
|
num, err := strconv.Atoi(body.Expires)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var add time.Duration
|
2021-03-02 11:00:18 +00:00
|
|
|
switch body.Unit {
|
2019-01-05 22:44:33 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2021-03-02 11:00:18 +00:00
|
|
|
hash, status, err := getSharePasswordHash(body)
|
|
|
|
if err != nil {
|
|
|
|
return status, err
|
|
|
|
}
|
2024-02-10 00:13:02 +00:00
|
|
|
stringHash := ""
|
2021-03-02 11:00:18 +00:00
|
|
|
var token string
|
|
|
|
if len(hash) > 0 {
|
2023-10-09 22:24:48 +00:00
|
|
|
tokenBuffer := make([]byte, 24) //nolint:gomnd
|
2021-03-02 11:00:18 +00:00
|
|
|
if _, err := rand.Read(tokenBuffer); err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
token = base64.URLEncoding.EncodeToString(tokenBuffer)
|
2024-02-10 00:13:02 +00:00
|
|
|
stringHash = string(hash)
|
2021-03-02 11:00:18 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
path := r.URL.Query().Get("path")
|
2019-01-05 22:44:33 +00:00
|
|
|
s = &share.Link{
|
2024-11-21 00:15:30 +00:00
|
|
|
Path: path,
|
|
|
|
Hash: secure_hash,
|
2021-03-02 11:00:18 +00:00
|
|
|
Expire: expire,
|
|
|
|
UserID: d.user.ID,
|
2024-02-10 00:13:02 +00:00
|
|
|
PasswordHash: stringHash,
|
2021-03-02 11:00:18 +00:00
|
|
|
Token: token,
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
if err := store.Share.Save(s); err != nil {
|
2019-01-05 22:44:33 +00:00
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return renderJSON(w, r, s)
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2021-03-02 11:00:18 +00:00
|
|
|
|
|
|
|
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 {
|
2024-11-26 17:21:41 +00:00
|
|
|
return nil, http.StatusInternalServerError, fmt.Errorf("failed to hash password")
|
2021-03-02 11:00:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return hash, 0, nil
|
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
|
|
|
|
func generateShortUUID() (string, error) {
|
|
|
|
// Generate 16 random bytes (128 bits of entropy)
|
|
|
|
bytes := make([]byte, 16)
|
|
|
|
_, err := rand.Read(bytes)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encode the bytes to a URL-safe base64 string
|
|
|
|
uuid := base64.RawURLEncoding.EncodeToString(bytes)
|
|
|
|
|
|
|
|
// Trim the length to 22 characters for a shorter ID
|
|
|
|
return uuid[:22], nil
|
|
|
|
}
|