132 lines
3.3 KiB
Go
132 lines
3.3 KiB
Go
package files
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sort"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"tea.chunkbyte.com/kato/go-worm/lib/config"
|
||
|
|
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
|
||
|
|
"tea.chunkbyte.com/kato/go-worm/lib/models"
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
ErrInvalidPath = errors.New("path must be an absolute accessible Windows path")
|
||
|
|
ErrOutsideRoot = errors.New("path is outside AGENT_FILE_ROOT")
|
||
|
|
)
|
||
|
|
|
||
|
|
func AllowedPath(root, raw string, requireExisting bool) (string, error) {
|
||
|
|
if strings.TrimSpace(raw) == "" || !filepath.IsAbs(raw) {
|
||
|
|
return "", ErrInvalidPath
|
||
|
|
}
|
||
|
|
clean := filepath.Clean(raw)
|
||
|
|
if requireExisting {
|
||
|
|
resolved, err := CanonicalExistingPath(clean)
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
clean = resolved
|
||
|
|
}
|
||
|
|
if root != "" && !isWithin(root, clean) {
|
||
|
|
return "", ErrOutsideRoot
|
||
|
|
}
|
||
|
|
return clean, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func CanonicalExistingPath(path string) (string, error) {
|
||
|
|
abs, err := filepath.Abs(filepath.Clean(path))
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(abs); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
resolved, err := filepath.EvalSymlinks(abs)
|
||
|
|
if err == nil {
|
||
|
|
return filepath.Abs(resolved)
|
||
|
|
}
|
||
|
|
return abs, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func ParseDepth(raw string) (int, error) {
|
||
|
|
if raw == "" {
|
||
|
|
return 0, nil
|
||
|
|
}
|
||
|
|
depth, err := strconv.Atoi(raw)
|
||
|
|
if err != nil || (depth != 0 && depth != 1) {
|
||
|
|
return 0, errors.New("depth must be 0 or 1")
|
||
|
|
}
|
||
|
|
return depth, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListDirectory(dir string, depth int) ([]models.FileItem, error) {
|
||
|
|
result := make([]models.FileItem, 0)
|
||
|
|
var walk func(string, string, int) error
|
||
|
|
walk = func(current, relative string, remaining int) error {
|
||
|
|
entries, err := os.ReadDir(current)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, entry := range entries {
|
||
|
|
if len(result) >= config.MaxListEntries {
|
||
|
|
return errors.New("directory listing exceeds entry limit")
|
||
|
|
}
|
||
|
|
info, err := entry.Info()
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
entryPath := filepath.Join(current, entry.Name())
|
||
|
|
rel := filepath.Join(relative, entry.Name())
|
||
|
|
kind := "file"
|
||
|
|
if info.IsDir() {
|
||
|
|
kind = "dir"
|
||
|
|
}
|
||
|
|
result = append(result, models.FileItem{
|
||
|
|
Name: entry.Name(),
|
||
|
|
Path: rel,
|
||
|
|
Type: kind,
|
||
|
|
Size: info.Size(),
|
||
|
|
Modified: info.ModTime().UTC(),
|
||
|
|
})
|
||
|
|
if remaining > 0 && info.IsDir() && entry.Type()&os.ModeSymlink == 0 {
|
||
|
|
if err := walk(entryPath, rel, remaining-1); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if err := walk(dir, "", depth); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
sort.Slice(result, func(i, j int) bool {
|
||
|
|
return strings.ToLower(result[i].Path) < strings.ToLower(result[j].Path)
|
||
|
|
})
|
||
|
|
return result, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func WritePathError(w http.ResponseWriter, err error) {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, ErrInvalidPath):
|
||
|
|
helpers.WriteError(w, http.StatusBadRequest, err.Error())
|
||
|
|
case errors.Is(err, ErrOutsideRoot), errors.Is(err, os.ErrPermission):
|
||
|
|
helpers.WriteError(w, http.StatusForbidden, "path is not accessible")
|
||
|
|
case errors.Is(err, os.ErrNotExist):
|
||
|
|
helpers.WriteError(w, http.StatusNotFound, "path does not exist")
|
||
|
|
default:
|
||
|
|
helpers.WriteError(w, http.StatusBadRequest, "path is not accessible")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isWithin(root, candidate string) bool {
|
||
|
|
rel, err := filepath.Rel(root, candidate)
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) && !filepath.IsAbs(rel)
|
||
|
|
}
|