filebrowser/backend/http/middleware.go

330 lines
9.8 KiB
Go
Raw Normal View History

2024-11-21 00:15:30 +00:00
package http
import (
2024-12-02 17:14:50 +00:00
"compress/gzip"
2024-11-21 00:15:30 +00:00
"encoding/json"
"fmt"
"log"
"net/http"
"path/filepath"
2024-12-02 17:14:50 +00:00
"strings"
2024-11-21 00:15:30 +00:00
"time"
"github.com/golang-jwt/jwt/v4"
2024-12-17 00:01:55 +00:00
"github.com/gtsteffaniak/filebrowser/backend/files"
"github.com/gtsteffaniak/filebrowser/backend/runner"
"github.com/gtsteffaniak/filebrowser/backend/settings"
"github.com/gtsteffaniak/filebrowser/backend/users"
2024-11-21 00:15:30 +00:00
)
type requestContext struct {
user *users.User
*runner.Runner
raw interface{}
}
type HttpResponse struct {
Status int `json:"status,omitempty"`
Message string `json:"message,omitempty"`
Token string `json:"token,omitempty"`
}
2024-11-26 17:21:41 +00:00
var FileInfoFasterFunc = files.FileInfoFaster
2024-11-21 00:15:30 +00:00
// Updated handleFunc to match the new signature
type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error)
// Middleware to handle file requests by hash and pass it to the handler
func withHashFileHelper(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
path := r.URL.Query().Get("path")
hash := r.URL.Query().Get("hash")
data.user = &users.PublicUser
// Get the file link by hash
link, err := store.Share.GetByHash(hash)
if err != nil {
2024-11-26 17:21:41 +00:00
return http.StatusNotFound, fmt.Errorf("share not found")
2024-11-21 00:15:30 +00:00
}
// Authenticate the share request if needed
var status int
if link.Hash != "" {
status, err = authenticateShareRequest(r, link)
if err != nil || status != http.StatusOK {
2024-11-26 17:21:41 +00:00
return status, fmt.Errorf("could not authenticate share request")
2024-11-21 00:15:30 +00:00
}
}
// Retrieve the user (using the public user by default)
user := &users.PublicUser
// Get file information with options
2024-11-26 17:21:41 +00:00
file, err := FileInfoFasterFunc(files.FileOptions{
2024-11-21 00:15:30 +00:00
Path: filepath.Join(user.Scope, link.Path+"/"+path),
Modify: user.Perm.Modify,
Expand: true,
ReadHeader: config.Server.TypeDetectionByHeader,
Checker: user, // Call your checker function here
})
2024-11-26 17:21:41 +00:00
file.Token = link.Token
2024-11-21 00:15:30 +00:00
if err != nil {
2024-11-26 17:21:41 +00:00
return errToStatus(err), fmt.Errorf("error fetching share from server")
2024-11-21 00:15:30 +00:00
}
// Set the file info in the `data` object
data.raw = file
// Call the next handler with the data
return fn(w, r, data)
}
}
// Middleware to ensure the user is an admin
func withAdminHelper(fn handleFunc) handleFunc {
return withUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
// Ensure the user has admin permissions
if !data.user.Perm.Admin {
return http.StatusForbidden, nil
}
// Proceed to the actual handler if the user is admin
return fn(w, r, data)
})
}
// Middleware to retrieve and authenticate user
func withUserHelper(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
2024-12-02 17:14:50 +00:00
if settings.Config.Auth.Method == "noauth" {
var err error
// Retrieve the user from the store and store it in the context
data.user, err = store.Users.Get(config.Server.Root, "admin")
if err != nil {
return http.StatusInternalServerError, err
}
return fn(w, r, data)
}
2024-11-21 00:15:30 +00:00
keyFunc := func(token *jwt.Token) (interface{}, error) {
return config.Auth.Key, nil
}
tokenString, err := extractToken(r)
if err != nil {
return http.StatusUnauthorized, err
}
var tk users.AuthToken
token, err := jwt.ParseWithClaims(tokenString, &tk, keyFunc)
if err != nil {
return http.StatusUnauthorized, fmt.Errorf("error processing token, %v", err)
}
if !token.Valid {
return http.StatusUnauthorized, fmt.Errorf("invalid token")
}
if isRevokedApiKey(tk.Key) || tk.Expires < time.Now().Unix() {
return http.StatusUnauthorized, fmt.Errorf("token expired or revoked")
}
// Check if the token is about to expire and send a header to renew it
if tk.Expires < time.Now().Add(time.Hour).Unix() {
w.Header().Add("X-Renew-Token", "true")
}
// Retrieve the user from the store and store it in the context
data.user, err = store.Users.Get(config.Server.Root, tk.BelongsTo)
if err != nil {
return http.StatusInternalServerError, err
}
// Call the handler function, passing in the context
return fn(w, r, data)
}
}
// Middleware to ensure the user is either the requested user or an admin
func withSelfOrAdminHelper(fn handleFunc) handleFunc {
return withUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
// Check if the current user is the same as the requested user or if they are an admin
if !data.user.Perm.Admin {
return http.StatusForbidden, nil
}
// Call the actual handler function with the updated context
return fn(w, r, data)
})
}
func wrapHandler(fn handleFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := &requestContext{
Runner: &runner.Runner{Enabled: config.Server.EnableExec, Settings: config},
}
// Call the actual handler function and get status code and error
status, err := fn(w, r, data)
// Handle the error case if there is one
if err != nil {
// Create an error response in JSON format
response := &HttpResponse{
Status: status, // Use the status code from the middleware
Message: err.Error(),
}
// Set the content type to JSON and status code
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
// Marshal the error response to JSON
errorBytes, marshalErr := json.Marshal(response)
if marshalErr != nil {
log.Printf("Error marshalling error response: %v", marshalErr)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Write the JSON error response
if _, writeErr := w.Write(errorBytes); writeErr != nil {
log.Printf("Error writing error response: %v", writeErr)
}
return
}
// No error, proceed to write status if non-zero
if status != 0 {
w.WriteHeader(status)
}
}
}
func withPermShareHelper(fn handleFunc) handleFunc {
return withUserHelper(func(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if !d.user.Perm.Share {
return http.StatusForbidden, nil
}
return fn(w, r, d)
})
}
func withPermShare(fn handleFunc) http.HandlerFunc {
return wrapHandler(withPermShareHelper(fn))
}
// Example of wrapping specific middleware functions for use with http.HandleFunc
func withHashFile(fn handleFunc) http.HandlerFunc {
return wrapHandler(withHashFileHelper(fn))
}
func withAdmin(fn handleFunc) http.HandlerFunc {
return wrapHandler(withAdminHelper(fn))
}
func withUser(fn handleFunc) http.HandlerFunc {
return wrapHandler(withUserHelper(fn))
}
func withSelfOrAdmin(fn handleFunc) http.HandlerFunc {
return wrapHandler(withSelfOrAdminHelper(fn))
}
func muxWithMiddleware(mux *http.ServeMux) *http.ServeMux {
wrappedMux := http.NewServeMux()
wrappedMux.Handle("/", LoggingMiddleware(mux))
return wrappedMux
}
// ResponseWriterWrapper wraps the standard http.ResponseWriter to capture the status code
type ResponseWriterWrapper struct {
http.ResponseWriter
StatusCode int
wroteHeader bool
2024-12-02 17:14:50 +00:00
PayloadSize int
2024-11-21 00:15:30 +00:00
}
// WriteHeader captures the status code and ensures it's only written once
func (w *ResponseWriterWrapper) WriteHeader(statusCode int) {
if !w.wroteHeader { // Prevent WriteHeader from being called multiple times
if statusCode == 0 {
statusCode = http.StatusInternalServerError
}
w.StatusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
w.wroteHeader = true
}
}
// Write is the method to write the response body and ensure WriteHeader is called
func (w *ResponseWriterWrapper) Write(b []byte) (int, error) {
if !w.wroteHeader { // Default to 200 if WriteHeader wasn't called explicitly
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}
2024-12-02 17:14:50 +00:00
// LoggingMiddleware logs each request and its status code.
2024-11-21 00:15:30 +00:00
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2024-11-26 17:21:41 +00:00
2024-11-21 00:15:30 +00:00
start := time.Now()
2024-12-02 17:14:50 +00:00
// Wrap the ResponseWriter to capture the status code.
2024-11-21 00:15:30 +00:00
wrappedWriter := &ResponseWriterWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
2024-12-02 17:14:50 +00:00
// Call the next handler.
2024-11-21 00:15:30 +00:00
next.ServeHTTP(wrappedWriter, r)
2024-12-02 17:14:50 +00:00
// Determine the color based on the status code.
2024-11-21 00:15:30 +00:00
color := "\033[32m" // Default green color
if wrappedWriter.StatusCode >= 300 && wrappedWriter.StatusCode < 500 {
color = "\033[33m" // Yellow for client errors (4xx)
} else if wrappedWriter.StatusCode >= 500 {
color = "\033[31m" // Red for server errors (5xx)
}
2024-12-02 17:14:50 +00:00
// Capture the full URL path including the query parameters.
2024-11-21 00:15:30 +00:00
fullURL := r.URL.Path
if r.URL.RawQuery != "" {
fullURL += "?" + r.URL.RawQuery
}
2024-12-02 17:14:50 +00:00
// Log the request, status code, and response size.
2024-11-21 00:15:30 +00:00
log.Printf("%s%-7s | %3d | %-15s | %-12s | \"%s\"%s",
color,
r.Method,
2024-12-02 17:14:50 +00:00
wrappedWriter.StatusCode, // Captured status code
2024-11-21 00:15:30 +00:00
r.RemoteAddr,
time.Since(start).String(),
fullURL,
"\033[0m", // Reset color
)
})
}
2024-12-02 17:14:50 +00:00
func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) {
2024-11-21 00:15:30 +00:00
marsh, err := json.Marshal(data)
if err != nil {
return http.StatusInternalServerError, err
}
2024-12-02 17:14:50 +00:00
// Calculate size in KB
payloadSizeKB := len(marsh) / 1024
// Check if the client accepts gzip encoding and hasn't explicitly disabled it
if acceptsGzip(r) && payloadSizeKB > 10 {
// Enable gzip compression
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
if _, err := gz.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
} else {
// Normal response without compression
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
2024-11-21 00:15:30 +00:00
}
return 0, nil
}
2024-12-02 17:14:50 +00:00
func acceptsGzip(r *http.Request) bool {
ae := r.Header.Get("Accept-Encoding")
return ae != "" && strings.Contains(ae, "gzip")
}