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) }