From 19133752519294d32391e79445ba5467f973d6c6 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 21 Sep 2026 23:35:11 +0800 Subject: [PATCH] feat: enforce folder download policy --- chfs-ng/cmd/chfs/main.go | 2 +- chfs-ng/internal/api/folder_download_test.go | 55 ++++++++++++++++++++ chfs-ng/internal/api/server.go | 22 ++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 chfs-ng/internal/api/folder_download_test.go diff --git a/chfs-ng/cmd/chfs/main.go b/chfs-ng/cmd/chfs/main.go index 3c211bd..65087e4 100644 --- a/chfs-ng/cmd/chfs/main.go +++ b/chfs-ng/cmd/chfs/main.go @@ -89,7 +89,7 @@ func main() { if len(cfg.Path) == 1 { store = storage.Local{Root: root} } - srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, SessionExpiry: map[string]time.Time{}, SessionTimeout: cfg.SessionTimeout, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1, RemoveMode: cfg.FileRemove, ImagePreview: cfg.ImagePreview, Log: opslog.New(cfg.Log)} + srv := &api.Server{Store: store, Auth: mgr, Sessions: map[string]*auth.Account{}, SessionExpiry: map[string]time.Time{}, SessionTimeout: cfg.SessionTimeout, WebDAVRoot: root, Allow: cfg.Allow, MultiRoot: len(cfg.Path) > 1, RemoveMode: cfg.FileRemove, ImagePreview: cfg.ImagePreview, FolderDownload: cfg.FolderDownload, Log: opslog.New(cfg.Log)} addr := ":" + itoa(cfg.Port) log.Printf("chfs-ng listening on %s root=%s removeMode=%d", addr, filepath.Clean(root), cfg.FileRemove) if cfg.SSLCert != "" && cfg.SSLKey != "" { diff --git a/chfs-ng/internal/api/folder_download_test.go b/chfs-ng/internal/api/folder_download_test.go new file mode 100644 index 0000000..0c88518 --- /dev/null +++ b/chfs-ng/internal/api/folder_download_test.go @@ -0,0 +1,55 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "chfs-ng/internal/auth" + "chfs-ng/internal/storage" +) + +func TestFolderDownloadModes(t *testing.T) { + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "parent", "child"), 0o755); err != nil { + t.Fatal(err) + } + for _, p := range []string{ + filepath.Join(root, "parent", "a.txt"), + filepath.Join(root, "parent", "child", "b.txt"), + } { + if err := os.WriteFile(p, []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + } + mk := func(mode string) *Server { + return &Server{ + Store: storage.Local{Root: root}, + Auth: &auth.Manager{ + Accounts: map[string]*auth.Account{}, + Guest: &auth.Account{Mask: auth.Read | auth.Write | auth.Delete}, + }, + FolderDownload: mode, + } + } + for _, tc := range []struct { + mode, path string + want int + }{ + {"disable", "parent", http.StatusForbidden}, + {"leaf", "parent", http.StatusForbidden}, + {"leaf", "parent/child", http.StatusOK}, + {"enable", "parent", http.StatusOK}, + } { + t.Run(tc.mode+"-"+tc.path, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/chfs/downloaddir/"+tc.path, nil) + w := httptest.NewRecorder() + mk(tc.mode).Handler().ServeHTTP(w, req) + if w.Code != tc.want { + t.Fatalf("status=%d want=%d body=%s", w.Code, tc.want, w.Body.String()) + } + }) + } +} diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 7074340..c224e91 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -43,6 +43,7 @@ type Server struct { MultiRoot bool RemoveMode int ImagePreview bool + FolderDownload string Log *opslog.Logger } type fileInfo struct { @@ -815,6 +816,27 @@ func (s *Server) downloadDir(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } + mode := strings.ToLower(strings.TrimSpace(s.FolderDownload)) + if mode == "" { + mode = "enable" + } + if mode == "disable" || mode == "false" || mode == "0" { + writeJSONError(w, http.StatusForbidden, "folder download disabled") + return + } + if mode == "leaf" { + items, err := s.Store.List(p) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + for _, item := range items { + if item.IsDir() { + writeJSONError(w, http.StatusForbidden, "folder download is only available for leaf directories") + return + } + } + } name := fi.Name() if name == "." || name == "" { name = "download"