From b92f1face5d19fa1f6800a3aa6c2f8f80e3f2278 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 02:53:19 +0000 Subject: [PATCH] feat: add native WebDAV submodule and usability helpers This commit introduces a new, high-performance, and extensible WebDAV submodule, implemented natively without external dependencies. It also adds a high-level API to simplify common use cases. The submodule includes: - A core WebDAV handler that supports essential methods: PROPFIND, MKCOL, GET, PUT, DELETE, COPY, MOVE, LOCK, and UNLOCK. - An extensible design using a `FileSystem` interface to decouple the protocol logic from the storage backend. - Two `FileSystem` implementations: - `MemFS`: An in-memory, tree-based filesystem for testing and ephemeral storage. - `OSFS`: A secure, OS-based filesystem that interacts with the local disk, including robust path traversal and symlink protection. - A `LockSystem` interface with an in-memory implementation (`MemLock`) that supports resource locking and includes a graceful shutdown mechanism. - A high-level API in `webdav/easy.go` (`Serve`, `Register`) to simplify serving local directories. - RFC 4918 compliance for core operations. - Performance optimizations, including `sync.Pool` for object reuse and `sync/atomic` for lock-free field access. - Comprehensive unit tests and a working example application. The Touka framework's core has been updated to recognize all WebDAV-specific HTTP methods. This implementation addresses numerous points from detailed code reviews, including security vulnerabilities, memory leaks, RFC compliance issues, and path handling bugs. --- examples/webdav/main.go | 12 ++-------- touka.go | 4 ++++ webdav/easy.go | 48 +++++++++++++++++++++++++++++++++++++++ webdav/easy_test.go | 50 +++++++++++++++++++++++++++++++++++++++++ webdav/osfs.go | 6 ++++- webdav/webdav.go | 16 ++++++------- 6 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 webdav/easy.go create mode 100644 webdav/easy_test.go diff --git a/examples/webdav/main.go b/examples/webdav/main.go index ecbfe15..87bb00e 100644 --- a/examples/webdav/main.go +++ b/examples/webdav/main.go @@ -17,18 +17,10 @@ func main() { log.Fatal(err) } - // Create a new WebDAV handler with the OS file system. - fs, err := webdav.NewOSFS("public") - if err != nil { + // Serve the "public" directory on the "/webdav/" route. + if err := webdav.Serve(r, "/webdav", "public"); err != nil { log.Fatal(err) } - handler := webdav.NewHandler("/webdav", fs, webdav.NewMemLock(), log.New(os.Stdout, "", 0)) - - // Mount the WebDAV handler on the "/webdav/" route. - webdavMethods := []string{ - "OPTIONS", "GET", "HEAD", "DELETE", "PUT", "MKCOL", "COPY", "MOVE", "PROPFIND", "PROPPATCH", "LOCK", "UNLOCK", - } - r.HandleFunc(webdavMethods, "/webdav/*path", handler.ServeTouka) log.Println("Touka WebDAV Server starting on :8080...") if err := r.RunShutdown(":8080", 10*time.Second); err != nil { diff --git a/touka.go b/touka.go index 898a8b3..441212c 100644 --- a/touka.go +++ b/touka.go @@ -67,6 +67,8 @@ var ( MethodMkcol = "MKCOL" MethodCopy = "COPY" MethodMove = "MOVE" + MethodLock = "LOCK" + MethodUnlock = "UNLOCK" ) var MethodsSet = map[string]struct{}{ @@ -84,4 +86,6 @@ var MethodsSet = map[string]struct{}{ MethodMkcol: {}, MethodCopy: {}, MethodMove: {}, + MethodLock: {}, + MethodUnlock: {}, } diff --git a/webdav/easy.go b/webdav/easy.go new file mode 100644 index 0000000..56d1eb0 --- /dev/null +++ b/webdav/easy.go @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// Copyright 2024 WJQSERVER. All rights reserved. +// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. +package webdav + +import ( + "log" + "os" + + "github.com/infinite-iroha/touka" +) + +// Config is a configuration for the WebDAV handler. +type Config struct { + FileSystem FileSystem + LockSystem LockSystem + Logger Logger +} + +// Register registers a WebDAV handler on the given router. +func Register(engine *touka.Engine, prefix string, cfg *Config) { + if cfg.LockSystem == nil { + cfg.LockSystem = NewMemLock() + } + + handler := NewHandler(prefix, cfg.FileSystem, cfg.LockSystem, cfg.Logger) + + webdavMethods := []string{ + "OPTIONS", "GET", "HEAD", "DELETE", "PUT", "MKCOL", "COPY", "MOVE", "PROPFIND", "PROPPATCH", "LOCK", "UNLOCK", + } + engine.HandleFunc(webdavMethods, prefix+"/*path", handler.ServeTouka) +} + +// Serve serves a local directory via WebDAV. +func Serve(engine *touka.Engine, prefix string, rootDir string) error { + fs, err := NewOSFS(rootDir) + if err != nil { + return err + } + + cfg := &Config{ + FileSystem: fs, + Logger: log.New(os.Stdout, "", 0), + } + Register(engine, prefix, cfg) + return nil +} diff --git a/webdav/easy_test.go b/webdav/easy_test.go new file mode 100644 index 0000000..bf44441 --- /dev/null +++ b/webdav/easy_test.go @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// Copyright 2024 WJQSERVER. All rights reserved. +// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. +package webdav + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/infinite-iroha/touka" +) + +func TestRegister(t *testing.T) { + r := touka.New() + cfg := &Config{ + FileSystem: NewMemFS(), + } + Register(r, "/dav", cfg) + + // Check if a WebDAV method is registered + req, _ := http.NewRequest("PROPFIND", "/dav/", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code == http.StatusNotFound { + t.Errorf("Expected PROPFIND to be registered, but got 404") + } +} + +func TestServe(t *testing.T) { + r := touka.New() + dir, _ := os.MkdirTemp("", "webdav") + defer os.RemoveAll(dir) + + if err := Serve(r, "/serve", dir); err != nil { + t.Fatalf("Serve failed: %v", err) + } + + // Check if a WebDAV method is registered + req, _ := http.NewRequest("OPTIONS", "/serve/", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected OPTIONS to return 200, but got %d", w.Code) + } +} diff --git a/webdav/osfs.go b/webdav/osfs.go index 30c76ec..3d54a30 100644 --- a/webdav/osfs.go +++ b/webdav/osfs.go @@ -55,7 +55,11 @@ func (fs *OSFS) resolve(name string) (string, error) { } } - if !strings.HasPrefix(path, fs.RootDir) { + rel, err := filepath.Rel(fs.RootDir, path) + if err != nil { + return "", err + } + if strings.HasPrefix(rel, "..") { return "", os.ErrPermission } diff --git a/webdav/webdav.go b/webdav/webdav.go index fd1d741..b438c24 100644 --- a/webdav/webdav.go +++ b/webdav/webdav.go @@ -397,7 +397,7 @@ func (h *Handler) handleCopy(c *touka.Context) { return } - if err := h.copy(c.Context(), srcPath.(string), destPath); err != nil { + if err := h.copy(c, srcPath.(string), destPath); err != nil { c.Status(http.StatusInternalServerError) return } @@ -450,18 +450,18 @@ func (h *Handler) handleMove(c *touka.Context) { } } -func (h *Handler) copy(ctx context.Context, src, dest string) error { - info, err := h.FileSystem.Stat(ctx, src) +func (h *Handler) copy(c *touka.Context, src, dest string) error { + info, err := h.FileSystem.Stat(c.Context(), src) if err != nil { return err } if info.IsDir() { - if err := h.FileSystem.Mkdir(ctx, dest, info.Mode()); err != nil { + if err := h.FileSystem.Mkdir(c.Context(), dest, info.Mode()); err != nil { return err } - srcFile, err := h.FileSystem.OpenFile(&touka.Context{Request: &http.Request{}}, src, os.O_RDONLY, 0) + srcFile, err := h.FileSystem.OpenFile(c, src, os.O_RDONLY, 0) if err != nil { return err } @@ -473,20 +473,20 @@ func (h *Handler) copy(ctx context.Context, src, dest string) error { } for _, child := range children { - if err := h.copy(ctx, path.Join(src, child.Name()), path.Join(dest, child.Name())); err != nil { + if err := h.copy(c, path.Join(src, child.Name()), path.Join(dest, child.Name())); err != nil { return err } } return nil } - srcFile, err := h.FileSystem.OpenFile(&touka.Context{Request: &http.Request{}}, src, os.O_RDONLY, 0) + srcFile, err := h.FileSystem.OpenFile(c, src, os.O_RDONLY, 0) if err != nil { return err } defer srcFile.Close() - destFile, err := h.FileSystem.OpenFile(&touka.Context{Request: &http.Request{}}, dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) + destFile, err := h.FileSystem.OpenFile(c, dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) if err != nil { return err }