gohttp/handler/file.go

109 lines
2.8 KiB
Go

package handler
import (
"errors"
"fmt"
"io/fs"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"git.kingecg.top/kingecg/gologger"
)
type FileHandler struct {
WPath string
Root string
Default string
http.FileSystem
}
func (f FileHandler) String() string {
return fmt.Sprintf("FileHandler:{WPath:%s,Root:%s,Default:%s}", f.WPath, f.Root, f.Default)
}
func (f FileHandler) Open(name string) (http.File, error) {
l := gologger.GetLogger("filehandler")
l.Debug("Open called: name=", name)
if strings.HasPrefix(name, "../") {
return nil, errors.New("not permitted")
}
relatedPath := strings.TrimPrefix(name, f.WPath)
// When URL ends with "/" (like "/") and WPath matches, we should return
// the directory and let http.FileServer handle the redirect to default file.
// This prevents "http: attempting to traverse a non-directory" error.
if relatedPath == "" && f.Default != "" {
relatedPath = "."
}
// If relatedPath becomes ".", use Root directly to represent the directory itself
var rPath string
if relatedPath == "." {
rPath = f.Root
} else {
rPath = filepath.Join(f.Root, strings.TrimPrefix(relatedPath, "/"))
}
// Resolve symlinks and clean path to prevent path traversal
realRoot, _ := filepath.EvalSymlinks(f.Root)
realRPath, _ := filepath.EvalSymlinks(rPath)
// Check if the resolved path is within root
if realRPath != realRoot && !strings.HasPrefix(realRPath, realRoot+string(filepath.Separator)) {
return nil, errors.New("not permitted")
}
fInfo, exists, err := FileExists(rPath)
if err != nil {
l.Error("access file error:", rPath, err)
return nil, err
}
if !exists {
return nil, os.ErrNotExist
}
// If it's a directory and not the root, don't allow listing
if fInfo.IsDir() && rPath != f.Root {
return nil, errors.New("not permit list dir")
}
// Use Lstat to check if the path itself is a symlink (doesn't follow symlinks)
if lstatInfo, err := os.Lstat(rPath); err == nil && lstatInfo.Mode()&fs.ModeSymlink != 0 {
return nil, errors.New("not permit follow symbol link")
}
fp, err := os.Open(rPath)
if err != nil {
l.Error("open file fail", err)
return nil, err
}
return fp, nil
}
func FileExists(name string) (os.FileInfo, bool, error) {
info, err := os.Stat(name)
if err != nil {
if os.IsNotExist(err) {
return nil, false, nil
}
return nil, false, err
}
return info, true, nil
}
func NewFileHandler(f FileHandler) http.Handler {
return http.FileServer(f)
}
func init() {
mime.AddExtensionType(".js", "application/javascript")
mime.AddExtensionType(".css", "text/css")
mime.AddExtensionType(".html", "text/html")
mime.AddExtensionType(".htm", "text/html")
mime.AddExtensionType(".svg", "image/svg+xml")
mime.AddExtensionType(".png", "image/png")
mime.AddExtensionType(".mjs", "application/javascript")
}