2019-01-05 22:44:33 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2024-11-26 17:21:41 +00:00
|
|
|
libError "errors"
|
2024-11-21 00:15:30 +00:00
|
|
|
"fmt"
|
2019-01-05 22:44:33 +00:00
|
|
|
"net/http"
|
2024-11-26 17:21:41 +00:00
|
|
|
"net/url"
|
2019-01-05 22:44:33 +00:00
|
|
|
"os"
|
|
|
|
"strings"
|
2024-11-21 00:15:30 +00:00
|
|
|
"sync"
|
2019-01-05 22:44:33 +00:00
|
|
|
"time"
|
|
|
|
|
2025-02-16 14:07:38 +00:00
|
|
|
jwt "github.com/golang-jwt/jwt/v4"
|
2022-05-03 20:48:45 +00:00
|
|
|
"github.com/golang-jwt/jwt/v4/request"
|
2024-11-26 17:21:41 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2020-05-31 23:12:36 +00:00
|
|
|
|
2024-12-17 00:01:55 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/errors"
|
2025-01-05 19:05:33 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/files"
|
2025-01-21 14:02:43 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/logger"
|
2024-12-17 00:01:55 +00:00
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/settings"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/share"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/users"
|
|
|
|
"github.com/gtsteffaniak/filebrowser/backend/utils"
|
2019-01-05 22:44:33 +00:00
|
|
|
)
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
var (
|
|
|
|
revokedApiKeyList map[string]bool
|
|
|
|
revokeMu sync.Mutex
|
|
|
|
)
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// first checks for cookie
|
|
|
|
// then checks for header Authorization as Bearer token
|
|
|
|
// then checks for query parameter
|
|
|
|
func extractToken(r *http.Request) (string, error) {
|
|
|
|
hasToken := false
|
|
|
|
tokenObj, err := r.Cookie("auth")
|
|
|
|
if err == nil {
|
|
|
|
hasToken = true
|
|
|
|
token := tokenObj.Value
|
|
|
|
// Checks if the token isn't empty and if it contains two dots.
|
|
|
|
// The former prevents incompatibility with URLs that previously
|
|
|
|
// used basic auth.
|
|
|
|
if token != "" && strings.Count(token, ".") == 2 {
|
|
|
|
return token, nil
|
2022-07-18 22:39:02 +00:00
|
|
|
}
|
2021-04-19 12:49:40 +00:00
|
|
|
}
|
|
|
|
|
2025-01-21 14:02:43 +00:00
|
|
|
auth := r.URL.Query().Get("auth")
|
|
|
|
if auth != "" {
|
|
|
|
hasToken = true
|
|
|
|
if strings.Count(auth, ".") == 2 {
|
|
|
|
return auth, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
// Check for Authorization header
|
|
|
|
authHeader := r.Header.Get("Authorization")
|
|
|
|
if authHeader != "" {
|
2025-01-21 14:02:43 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
hasToken = true
|
|
|
|
// Split the header to get "Bearer {token}"
|
|
|
|
parts := strings.Split(authHeader, " ")
|
|
|
|
if len(parts) == 2 && parts[0] == "Bearer" {
|
|
|
|
token := parts[1]
|
|
|
|
return token, nil
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
if hasToken {
|
|
|
|
return "", fmt.Errorf("invalid token provided")
|
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
return "", request.ErrNoTokenInRequest
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
2025-02-01 13:10:46 +00:00
|
|
|
if !config.Auth.Methods.PasswordAuth.Enabled {
|
2025-01-31 20:26:21 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// currently only supports user/pass
|
2024-11-21 00:15:30 +00:00
|
|
|
// Get the authentication method from the settings
|
2025-01-31 20:26:21 +00:00
|
|
|
auther, err := store.Auth.Get("password")
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
// Authenticate the user based on the request
|
|
|
|
user, err := auther.Auth(r, store.Users)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err == os.ErrPermission {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
} else if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
status, err := printToken(w, r, user) // Pass the data object
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, http.StatusText(status), status)
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type signupBody struct {
|
|
|
|
Username string `json:"username"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
func signupHandler(w http.ResponseWriter, r *http.Request) {
|
2023-12-01 23:47:00 +00:00
|
|
|
if !settings.Config.Auth.Signup {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if r.Body == nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
info := &signupBody{}
|
|
|
|
err := json.NewDecoder(r.Body).Decode(info)
|
|
|
|
if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if info.Password == "" || info.Username == "" {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
user := settings.ApplyUserDefaults(users.User{})
|
2024-09-16 21:01:16 +00:00
|
|
|
user.Username = info.Username
|
|
|
|
user.Password = info.Password
|
|
|
|
|
2025-01-05 19:05:33 +00:00
|
|
|
userHome, err := config.MakeUserDir(user.Username, user.Scope, files.RootPaths["default"])
|
2019-06-21 10:43:21 +00:00
|
|
|
if err != nil {
|
2025-01-21 14:02:43 +00:00
|
|
|
logger.Error(fmt.Sprintf("create user: failed to mkdir user home dir: [%s]", userHome))
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
return
|
2019-06-21 10:43:21 +00:00
|
|
|
}
|
|
|
|
user.Scope = userHome
|
2025-01-21 14:02:43 +00:00
|
|
|
logger.Debug(fmt.Sprintf("new user: %s, home dir: [%s].", user.Username, userHome))
|
2024-11-21 00:15:30 +00:00
|
|
|
err = store.Users.Save(&user)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err == errors.ErrExist {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusConflict), http.StatusConflict)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
} else if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
return
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
func renewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
|
|
|
// check if x-auth header is present and token is
|
|
|
|
return printToken(w, r, d.user)
|
|
|
|
}
|
2019-01-05 22:44:33 +00:00
|
|
|
|
2024-11-21 00:15:30 +00:00
|
|
|
func printToken(w http.ResponseWriter, _ *http.Request, user *users.User) (int, error) {
|
2025-01-27 00:21:12 +00:00
|
|
|
|
|
|
|
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*time.Duration(config.Auth.TokenExpirationHours), user.Perm)
|
2023-12-20 20:44:25 +00:00
|
|
|
if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
if strings.Contains(err.Error(), "key already exists with same name") {
|
|
|
|
return http.StatusConflict, err
|
|
|
|
}
|
|
|
|
return http.StatusInternalServerError, err
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
if _, err := w.Write([]byte(signed.Key)); err != nil {
|
|
|
|
return http.StatusInternalServerError, err
|
2023-12-20 20:44:25 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func isRevokedApiKey(key string) bool {
|
|
|
|
_, exists := revokedApiKeyList[key]
|
|
|
|
return exists
|
|
|
|
}
|
|
|
|
|
|
|
|
func revokeAPIKey(key string) {
|
|
|
|
revokeMu.Lock()
|
|
|
|
delete(revokedApiKeyList, key)
|
|
|
|
revokeMu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeSignedTokenAPI(user *users.User, name string, duration time.Duration, perms users.Permissions) (users.AuthToken, error) {
|
|
|
|
_, ok := user.ApiKeys[name]
|
|
|
|
if ok {
|
|
|
|
return users.AuthToken{}, fmt.Errorf("key already exists with same name %v ", name)
|
|
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
expires := now.Add(duration)
|
|
|
|
claim := users.AuthToken{
|
|
|
|
Permissions: perms,
|
|
|
|
Created: now.Unix(),
|
|
|
|
Expires: expires.Unix(),
|
|
|
|
Name: name,
|
|
|
|
BelongsTo: user.ID,
|
2022-06-13 14:13:10 +00:00
|
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
2024-11-21 00:15:30 +00:00
|
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
|
|
ExpiresAt: jwt.NewNumericDate(expires),
|
|
|
|
Issuer: "FileBrowser Quantum",
|
2019-01-05 22:44:33 +00:00
|
|
|
},
|
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
|
|
|
|
tokenString, err := token.SignedString(config.Auth.Key)
|
2019-01-05 22:44:33 +00:00
|
|
|
if err != nil {
|
2024-11-21 00:15:30 +00:00
|
|
|
return claim, err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
claim.Key = tokenString
|
|
|
|
if strings.HasPrefix(name, "WEB_TOKEN") {
|
|
|
|
// don't add to api tokens, its a short lived web token
|
|
|
|
return claim, err
|
2020-05-31 23:12:36 +00:00
|
|
|
}
|
2024-11-21 00:15:30 +00:00
|
|
|
// Perform the user update
|
|
|
|
err = store.Users.AddApiKey(user.ID, name, claim)
|
|
|
|
if err != nil {
|
|
|
|
return claim, err
|
|
|
|
}
|
|
|
|
return claim, err
|
2019-01-05 22:44:33 +00:00
|
|
|
}
|
2024-11-26 17:21:41 +00:00
|
|
|
|
|
|
|
func authenticateShareRequest(r *http.Request, l *share.Link) (int, error) {
|
|
|
|
if l.PasswordHash == "" {
|
|
|
|
return 200, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.URL.Query().Get("token") == l.Token {
|
|
|
|
return 200, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
password := r.Header.Get("X-SHARE-PASSWORD")
|
|
|
|
password, err := url.QueryUnescape(password)
|
|
|
|
if err != nil {
|
|
|
|
return http.StatusUnauthorized, err
|
|
|
|
}
|
|
|
|
if password == "" {
|
|
|
|
return http.StatusUnauthorized, nil
|
|
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(l.PasswordHash), []byte(password)); err != nil {
|
|
|
|
if libError.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
|
|
|
return http.StatusUnauthorized, nil
|
|
|
|
}
|
|
|
|
return 401, err
|
|
|
|
}
|
|
|
|
return 200, nil
|
|
|
|
}
|