mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-09 10:57:08 +02:00
The gate ran inside saveFn, so a request destined for 403 still spooled up to 10MB of multipart body to temp first. Artist, playlist and radio already gate before parsing; the shared handler now takes the gate as a parameter.
128 lines
3.9 KiB
Go
128 lines
3.9 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image"
|
|
"image/png"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/core/artwork"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
func testRouter(ds model.DataStore) http.Handler {
|
|
api := &Router{ds: ds, imgUpload: artwork.NewUploader(ds)}
|
|
r := chi.NewRouter()
|
|
api.addUserRoute(r)
|
|
return r
|
|
}
|
|
|
|
type trackingReader struct {
|
|
r io.Reader
|
|
read bool
|
|
}
|
|
|
|
func (t *trackingReader) Read(p []byte) (int, error) {
|
|
t.read = true
|
|
return t.r.Read(p)
|
|
}
|
|
|
|
var _ = Describe("User avatar routes", func() {
|
|
var router http.Handler
|
|
var ds *tests.MockDataStore
|
|
|
|
newRequest := func(method, path string, body io.Reader, contentType string, asUser model.User) *http.Request {
|
|
r := httptest.NewRequest(method, path, body)
|
|
if contentType != "" {
|
|
r.Header.Set("Content-Type", contentType)
|
|
}
|
|
return r.WithContext(request.WithUser(r.Context(), asUser))
|
|
}
|
|
|
|
pngUpload := func() (io.Reader, string) {
|
|
var buf bytes.Buffer
|
|
mw := multipart.NewWriter(&buf)
|
|
part, err := mw.CreateFormFile("image", "avatar.png")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(png.Encode(part, image.NewRGBA(image.Rect(0, 0, 8, 8)))).To(Succeed())
|
|
Expect(mw.Close()).To(Succeed())
|
|
return &buf, mw.FormDataContentType()
|
|
}
|
|
|
|
regularUser := model.User{ID: "u1", UserName: "regular"}
|
|
adminUser := model.User{ID: "admin", UserName: "admin", IsAdmin: true}
|
|
otherUser := model.User{ID: "u2", UserName: "other"}
|
|
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.DataFolder = conf.NewDir(GinkgoT().TempDir())
|
|
conf.Server.EnableUserAvatarUpload = true
|
|
ds = &tests.MockDataStore{}
|
|
Expect(ds.User(context.Background()).Put(®ularUser)).To(Succeed())
|
|
Expect(ds.User(context.Background()).Put(&adminUser)).To(Succeed())
|
|
Expect(ds.User(context.Background()).Put(&otherUser)).To(Succeed())
|
|
router = testRouter(ds)
|
|
})
|
|
|
|
It("lets a user upload their own avatar", func() {
|
|
body, ct := pngUpload()
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, newRequest("POST", "/user/u1/image", body, ct, regularUser))
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
usr, _ := ds.User(context.Background()).Get("u1")
|
|
Expect(usr.UploadedImage).To(Equal("u1_regular.png"))
|
|
})
|
|
|
|
It("lets an admin upload someone else's avatar", func() {
|
|
body, ct := pngUpload()
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, newRequest("POST", "/user/u1/image", body, ct, adminUser))
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("refuses a third party", func() {
|
|
body, ct := pngUpload()
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, newRequest("POST", "/user/u1/image", body, ct, otherUser))
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
})
|
|
|
|
It("refuses a third party before reading the request body", func() {
|
|
body, ct := pngUpload()
|
|
tracked := &trackingReader{r: body}
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, newRequest("POST", "/user/u1/image", tracked, ct, otherUser))
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
Expect(tracked.read).To(BeFalse())
|
|
})
|
|
|
|
It("refuses everyone, admins included, when the flag is off", func() {
|
|
conf.Server.EnableUserAvatarUpload = false
|
|
body, ct := pngUpload()
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, newRequest("POST", "/user/u1/image", body, ct, adminUser))
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
})
|
|
|
|
It("clears the filename on delete", func() {
|
|
Expect(ds.User(context.Background()).UpdateImage("u1", "u1_regular.png")).To(Succeed())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, newRequest("DELETE", "/user/u1/image", nil, "", regularUser))
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
usr, _ := ds.User(context.Background()).Get("u1")
|
|
Expect(usr.UploadedImage).To(BeEmpty())
|
|
})
|
|
})
|