109 lines
2.8 KiB
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")
|
|
}
|