mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
WebDAV: Route request errors to the system log, not the UI #5715
All WebDAV write-method errors (PUT/DELETE/MOVE/COPY/MKCOL/...) now go to the console-only system log instead of the in-app errors table and browser log viewer, because golang.org/x/net/webdav embeds absolute server paths in its messages. Operators still see full detail in the container/service logs. A MKCOL against an already-existing collection is a benign probe used by sync clients such as PhotoSync to test for a directory before creating it; it returns 405 and is logged at debug rather than as an error.
This commit is contained in:
parent
d33bca4f48
commit
7bea2dccfe
3 changed files with 70 additions and 6 deletions
|
|
@ -57,6 +57,7 @@
|
|||
- WebDAV response behavior:
|
||||
- Built-in security middleware skips browser-document headers (`Content-Security-Policy`, `X-Frame-Options`) on `/originals` and `/import` paths.
|
||||
- PROPFIND `207 Multi-Status` responses normalize XML media type to `application/xml; charset=utf-8`.
|
||||
- Request errors go to the console-only system log (`event.System*`), not the browser log stream, since `x/net/webdav` embeds absolute server paths in its messages. A `MKCOL` on an existing collection is a benign sync-client probe: it returns 405 and is logged at debug rather than as an error.
|
||||
- AutoTLS: uses `autocert` and spins up a redirect listener; ensure ports 80/443 are reachable.
|
||||
- Unix sockets: optional `force` query removes stale sockets; permissions can be set via `mode` query.
|
||||
- Health endpoints (`/livez`, `/health`, `/healthz`, `/readyz`) return `Cache-Control: no-store` and `Access-Control-Allow-Origin: *`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/api"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/workers/auto"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
|
@ -55,13 +57,19 @@ func WebDAV(dir string, router *gin.RouterGroup, conf *config.Config) {
|
|||
// Request logger function.
|
||||
loggerFunc := func(request *http.Request, err error) {
|
||||
if err != nil {
|
||||
switch request.Method {
|
||||
case header.MethodPut, header.MethodMkcol, header.MethodDelete, header.MethodMove, header.MethodCopy, header.MethodProppatch, header.MethodLock, header.MethodUnlock:
|
||||
log.Errorf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
|
||||
case header.MethodPropfind:
|
||||
log.Tracef("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
|
||||
// Route WebDAV request errors to the console-only system log, not log.*.
|
||||
// x/net/webdav embeds absolute originals/import paths in its messages,
|
||||
// which must stay out of the browser log viewer and the persisted errors
|
||||
// table; operators still see full detail in the server console.
|
||||
switch {
|
||||
case request.Method == header.MethodMkcol && errors.Is(err, os.ErrExist):
|
||||
// MKCOL on an existing collection is a benign probe: sync clients such as
|
||||
// PhotoSync test for a directory before creating it — expected, not a failure.
|
||||
event.SystemDebug([]string{"webdav", "collection %s already exists"}, clean.Log(request.URL.String()))
|
||||
case WebDAVWriteMethod(request.Method):
|
||||
event.SystemError([]string{"webdav", "%s in %s %s"}, clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
|
||||
default:
|
||||
log.Debugf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
|
||||
event.SystemDebug([]string{"webdav", "%s in %s %s"}, clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
|
||||
}
|
||||
} else {
|
||||
// Determine the filename if it is an uploaded file and process custom request headers, if any.
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
)
|
||||
|
||||
|
|
@ -64,6 +66,59 @@ func TestWebDAVWrite_MKCOL_PUT(t *testing.T) {
|
|||
assert.Equal(t, "hello", string(b))
|
||||
}
|
||||
|
||||
// logCapture is a logrus hook that records emitted entries for assertions.
|
||||
type logCapture struct{ entries []*logrus.Entry }
|
||||
|
||||
// Levels reports the log levels the capture hook fires on.
|
||||
func (h *logCapture) Levels() []logrus.Level { return logrus.AllLevels }
|
||||
|
||||
// Fire records the given log entry.
|
||||
func (h *logCapture) Fire(e *logrus.Entry) error {
|
||||
h.entries = append(h.entries, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWebDAVWrite_MKCOL_Exists(t *testing.T) {
|
||||
conf := newWebDAVTestConfig(t)
|
||||
if err := conf.CreateDirectories(); err != nil {
|
||||
t.Fatalf("failed to create test directories: %v", err)
|
||||
}
|
||||
r := setupWebDAVRouter(conf)
|
||||
|
||||
// First MKCOL creates the collection.
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(header.MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/exists", nil)
|
||||
authBearer(req)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
// Capture the console-only system log while repeating the MKCOL against the
|
||||
// existing collection. Errors from x/net/webdav are routed through
|
||||
// event.SystemLog (never the browser log.* stream), so we hook it here.
|
||||
hook := &logCapture{}
|
||||
event.SystemLog.ReplaceHooks(logrus.LevelHooks{})
|
||||
event.SystemLog.AddHook(hook)
|
||||
defer event.SystemLog.ReplaceHooks(logrus.LevelHooks{})
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(header.MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/exists", nil)
|
||||
authBearer(req)
|
||||
r.ServeHTTP(w, req)
|
||||
// Probing an existing collection is a client-side no-op; x/net/webdav returns 405.
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||
|
||||
// The benign probe must not be logged as an error, and must not leak the server path.
|
||||
var found bool
|
||||
for _, e := range hook.entries {
|
||||
if e.Level == logrus.DebugLevel && assert.Contains(t, e.Message, "already exists") {
|
||||
found = true
|
||||
assert.NotContains(t, e.Message, conf.OriginalsPath())
|
||||
}
|
||||
assert.NotEqual(t, logrus.ErrorLevel, e.Level, "MKCOL on existing collection must not log an error")
|
||||
}
|
||||
assert.True(t, found, "expected a debug entry for the existing collection")
|
||||
}
|
||||
|
||||
func TestWebDAVWrite_PUT_OriginalsLimit(t *testing.T) {
|
||||
conf := newWebDAVTestConfig(t)
|
||||
conf.Options().OriginalsLimit = 1 // cap uploaded files at 1 MB
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue