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, err := filepath.EvalSymlinks(f.Root) if err != nil { l.Error("failed to resolve root symlinks:", err) return nil, err } realRPath, err := filepath.EvalSymlinks(rPath) if err != nil { l.Error("failed to resolve path symlinks:", err) return nil, errors.New("not permitted") } // 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") }