mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
299 lines
11 KiB
Go
299 lines
11 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
"github.com/photoprism/photoprism/pkg/service/http/header"
|
|
)
|
|
|
|
func setupWebDAVRouter(conf *config.Config) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
grp := r.Group(conf.BaseUri(WebDAVOriginals), WebDAVAuth(conf))
|
|
WebDAV(conf.OriginalsPath(), grp, conf)
|
|
return r
|
|
}
|
|
|
|
func authBearer(req *http.Request) {
|
|
sess := entity.SessionFixtures.Get("alice_token_webdav")
|
|
header.SetAuthorization(req, sess.AuthToken())
|
|
}
|
|
|
|
func authBasic(req *http.Request) {
|
|
sess := entity.SessionFixtures.Get("alice_token_webdav")
|
|
basic := []byte(fmt.Sprintf("alice:%s", sess.AuthToken()))
|
|
req.Header.Set(header.Auth, fmt.Sprintf("%s %s", header.AuthBasic, base64.StdEncoding.EncodeToString(basic)))
|
|
}
|
|
|
|
func TestWebDAVWrite_MKCOL_PUT(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
|
|
// MKCOL
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/wdvdir", nil)
|
|
authBearer(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1) // Created
|
|
// PUT file
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/wdvdir/hello.txt", bytes.NewBufferString("hello"))
|
|
authBearer(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
// file exists
|
|
path := filepath.Join(conf.OriginalsPath(), "wdvdir", "hello.txt")
|
|
b, err := os.ReadFile(path)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "hello", string(b))
|
|
}
|
|
|
|
func TestWebDAVWrite_MOVE_COPY(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
|
|
// Ensure source and destination directories via MKCOL
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/src", nil)
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/dst", nil)
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
// Create source file via PUT
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/src/a.txt", bytes.NewBufferString("A"))
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
|
|
// MOVE /originals/src/a.txt -> /originals/dst/b.txt
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/src/a.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/b.txt")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
// Verify moved
|
|
assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "src", "a.txt"))
|
|
assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "dst", "b.txt"))
|
|
|
|
// COPY /originals/dst/b.txt -> /originals/dst/c.txt
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/dst/b.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/c.txt")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
// Verify copy
|
|
assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "dst", "b.txt"))
|
|
assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "dst", "c.txt"))
|
|
}
|
|
|
|
func TestWebDAVWrite_OverwriteSemantics(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
|
|
// Prepare src and dst
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "src"), 0o700)
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "dst"), 0o700)
|
|
_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "src", "f.txt"), []byte("NEW"), 0o600)
|
|
_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "dst", "f.txt"), []byte("OLD"), 0o600)
|
|
|
|
// COPY with Overwrite: F -> should not overwrite existing
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/src/f.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/f.txt")
|
|
req.Header.Set("Overwrite", "F")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
// Expect not successful (commonly 412 Precondition Failed)
|
|
if w.Code == 201 || w.Code == 204 {
|
|
t.Fatalf("expected failure when Overwrite=F, got %d", w.Code)
|
|
}
|
|
// Content remains OLD
|
|
b, _ := os.ReadFile(filepath.Join(conf.OriginalsPath(), "dst", "f.txt"))
|
|
assert.Equal(t, "OLD", string(b))
|
|
|
|
// COPY with Overwrite: T -> must overwrite
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/src/f.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/f.txt")
|
|
req.Header.Set("Overwrite", "T")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
// Success (201/204 acceptable)
|
|
if !(w.Code == 201 || w.Code == 204) {
|
|
t.Fatalf("expected success for Overwrite=T, got %d", w.Code)
|
|
}
|
|
b, _ = os.ReadFile(filepath.Join(conf.OriginalsPath(), "dst", "f.txt"))
|
|
assert.Equal(t, "NEW", string(b))
|
|
|
|
// MOVE with Overwrite: F to existing file -> expect failure
|
|
_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "src", "g.txt"), []byte("GNEW"), 0o600)
|
|
_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "dst", "g.txt"), []byte("GOLD"), 0o600)
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/src/g.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/g.txt")
|
|
req.Header.Set("Overwrite", "F")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
if w.Code == 201 || w.Code == 204 {
|
|
t.Fatalf("expected failure when Overwrite=F for MOVE, got %d", w.Code)
|
|
}
|
|
// MOVE with Overwrite: T -> overwrites and removes source
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/src/g.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst/g.txt")
|
|
req.Header.Set("Overwrite", "T")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
if !(w.Code == 201 || w.Code == 204) {
|
|
t.Fatalf("expected success for MOVE Overwrite=T, got %d", w.Code)
|
|
}
|
|
assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "src", "g.txt"))
|
|
gb, _ := os.ReadFile(filepath.Join(conf.OriginalsPath(), "dst", "g.txt"))
|
|
assert.Equal(t, "GNEW", string(gb))
|
|
}
|
|
|
|
func TestWebDAVWrite_MoveMissingDestination(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
// Ensure src exists
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "mv"), 0o700)
|
|
_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "mv", "file.txt"), []byte("X"), 0o600)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/mv/file.txt", nil)
|
|
// no Destination header
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
// Expect failure (not 201/204)
|
|
if w.Code == 201 || w.Code == 204 {
|
|
t.Fatalf("expected failure when Destination header missing, got %d", w.Code)
|
|
}
|
|
// Source remains
|
|
assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "mv", "file.txt"))
|
|
}
|
|
|
|
func TestWebDAVWrite_CopyInvalidDestinationPrefix(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
// Ensure src exists
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "cp"), 0o700)
|
|
_ = os.WriteFile(filepath.Join(conf.OriginalsPath(), "cp", "a.txt"), []byte("A"), 0o600)
|
|
|
|
// COPY to a destination outside the handler prefix
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/cp/a.txt", nil)
|
|
req.Header.Set("Destination", "/notwebdav/d.txt")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
// Expect failure
|
|
if w.Code == 201 || w.Code == 204 {
|
|
t.Fatalf("expected failure for invalid Destination prefix, got %d", w.Code)
|
|
}
|
|
// Destination not created
|
|
assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "notwebdav", "d.txt"))
|
|
}
|
|
|
|
func TestWebDAVWrite_MoveNonExistentSource(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
// Ensure destination dir exists
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "dst2"), 0o700)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/nosuch/file.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/dst2/file.txt")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
// Expect failure (e.g., 404)
|
|
if w.Code == 201 || w.Code == 204 {
|
|
t.Fatalf("expected failure moving non-existent source, got %d", w.Code)
|
|
}
|
|
assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "dst2", "file.txt"))
|
|
}
|
|
|
|
func TestWebDAVWrite_CopyTraversalDestination(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
|
|
// Create source file via PUT
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "travsrc"), 0o700)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/travsrc/a.txt", bytes.NewBufferString("A"))
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
|
|
// Attempt COPY with traversal in Destination
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodCopy, conf.BaseUri(WebDAVOriginals)+"/travsrc/a.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/../evil.txt")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
// Expect success with sanitized destination inside base
|
|
if !(w.Code == 201 || w.Code == 204) {
|
|
t.Fatalf("expected success (sanitized), got %d", w.Code)
|
|
}
|
|
// Not created above originals; created as /originals/evil.txt
|
|
parent := filepath.Dir(conf.OriginalsPath())
|
|
assert.NoFileExists(t, filepath.Join(parent, "evil.txt"))
|
|
assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "evil.txt"))
|
|
}
|
|
|
|
func TestWebDAVWrite_MoveTraversalDestination(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
r := setupWebDAVRouter(conf)
|
|
|
|
// Create source file via PUT
|
|
_ = os.MkdirAll(filepath.Join(conf.OriginalsPath(), "travsrc2"), 0o700)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodPut, conf.BaseUri(WebDAVOriginals)+"/travsrc2/a.txt", bytes.NewBufferString("A"))
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.InDelta(t, 201, w.Code, 1)
|
|
|
|
// Attempt MOVE with traversal in Destination
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(MethodMove, conf.BaseUri(WebDAVOriginals)+"/travsrc2/a.txt", nil)
|
|
req.Header.Set("Destination", conf.BaseUri(WebDAVOriginals)+"/../evil2.txt")
|
|
authBasic(req)
|
|
r.ServeHTTP(w, req)
|
|
if !(w.Code == 201 || w.Code == 204) {
|
|
t.Fatalf("expected success (sanitized) for MOVE, got %d", w.Code)
|
|
}
|
|
// Source removed; destination created inside base, not outside
|
|
assert.NoFileExists(t, filepath.Join(conf.OriginalsPath(), "travsrc2", "a.txt"))
|
|
parent := filepath.Dir(conf.OriginalsPath())
|
|
assert.NoFileExists(t, filepath.Join(parent, "evil2.txt"))
|
|
assert.FileExists(t, filepath.Join(conf.OriginalsPath(), "evil2.txt"))
|
|
}
|
|
|
|
func TestWebDAVWrite_ReadOnlyForbidden(t *testing.T) {
|
|
conf := config.TestConfig()
|
|
conf.Options().ReadOnly = true
|
|
r := setupWebDAVRouter(conf)
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(MethodMkcol, conf.BaseUri(WebDAVOriginals)+"/ro", nil)
|
|
authBearer(req)
|
|
r.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|