From 8061ef3cc394054e92ee57fbd80b7d0bbbbec7c5 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 10 Nov 2026 05:52:05 +0000 Subject: [PATCH] rproxy: pass WebDAV methods on (closes #278) rproxy now passes PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK and UNLOCK to discriminator instead of answering 405, so Nextcloud's Files app and sync client work. The Core Rule Set's 911100 still refuses them wherever no app's exclusions allow them. Model: opus-5-5 --- SPEC.md | 13 +++++++++++++ internal/rproxy/question_test.go | 7 +++++-- internal/rproxy/rproxy.go | 4 +++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/SPEC.md b/SPEC.md index 6592a45..78ecec5 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1881,10 +1881,23 @@ image runs it by default (see "Deployment"). - `DELETE` - `PATCH` - `QUERY` +- `PROPFIND` +- `PROPPATCH` +- `MKCOL` +- `COPY` +- `MOVE` +- `REPORT` +- `SEARCH` +- `LOCK` +- `UNLOCK` It answers any other method `405`, with no question and no report. The answer's `Allow` header names these methods. +Nextcloud's Files app and sync client use the last nine, which are WebDAV +methods. The Core Rule Set refuses them except where an app's exclusions allow +them. + `rproxy` answers `400`, with no question and no report, to a request with: - a target that does not start with `/`, such as `x:admin`, diff --git a/internal/rproxy/question_test.go b/internal/rproxy/question_test.go index 5940ab9..02c0cac 100644 --- a/internal/rproxy/question_test.go +++ b/internal/rproxy/question_test.go @@ -331,7 +331,7 @@ func TestAQuestionCarriesItsBodyWhateverTheMethod(t *testing.T) { func TestAMethodNotPassedOnIsAnswered405(t *testing.T) { t.Parallel() - for _, method := range []string{http.MethodTrace, "PROPFIND", "LOCK", "BREW"} { + for _, method := range []string{http.MethodTrace, "MKCALENDAR", "BREW"} { t.Run(method, func(t *testing.T) { t.Parallel() @@ -343,7 +343,8 @@ func TestAMethodNotPassedOnIsAnswered405(t *testing.T) { got := visit(t, newRequest(t, method, addr, "/", http.NoBody)) if got.status != http.StatusMethodNotAllowed || - got.header.Get("Allow") != "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY" { + got.header.Get("Allow") != "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, "+ + "PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK" { t.Errorf("answered %+v, want 405 naming the methods passed on", got) } @@ -368,6 +369,8 @@ func TestEachMethodPassedOnReachesDiscriminatorAndTheApp(t *testing.T) { for _, method := range []string{ http.MethodGet, http.MethodHead, http.MethodPost, http.MethodOptions, http.MethodPut, http.MethodDelete, http.MethodPatch, "QUERY", + "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "REPORT", "SEARCH", + "LOCK", "UNLOCK", } { t.Run(method, func(t *testing.T) { t.Parallel() diff --git a/internal/rproxy/rproxy.go b/internal/rproxy/rproxy.go index 0a4c9f0..94bf459 100644 --- a/internal/rproxy/rproxy.go +++ b/internal/rproxy/rproxy.go @@ -51,7 +51,9 @@ const ownEndpointPrefix = "/_sww/" // allowedMethods lists the methods rproxy passes on, as an Allow header gives // them. It answers any other method 405 itself. -const allowedMethods = "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY" +// The WebDAV ones are for Nextcloud. +const allowedMethods = "GET, HEAD, POST, OPTIONS, PUT, DELETE, PATCH, QUERY, " + + "PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, REPORT, SEARCH, LOCK, UNLOCK" // Params are what New needs. type Params struct {