diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 9ec4ce445..5fee7aa32 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -7,9 +7,7 @@ import ( "io" "time" - "github.com/lestrrat-go/jwx/v2/jwt" "github.com/navidrome/navidrome/core" - "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/ffmpeg" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -111,31 +109,3 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s } return artReader, err } - -func EncodeArtworkID(artID model.ArtworkID) string { - token, _ := auth.CreatePublicToken(map[string]any{"id": artID.String()}) - return token -} - -func DecodeArtworkID(tokenString string) (model.ArtworkID, error) { - token, err := auth.TokenAuth.Decode(tokenString) - if err != nil { - return model.ArtworkID{}, err - } - if token == nil { - return model.ArtworkID{}, errors.New("unauthorized") - } - err = jwt.Validate(token, jwt.WithRequiredClaim("id")) - if err != nil { - return model.ArtworkID{}, err - } - claims, err := token.AsMap(context.Background()) - if err != nil { - return model.ArtworkID{}, err - } - id, ok := claims["id"].(string) - if !ok { - return model.ArtworkID{}, errors.New("invalid id type") - } - return model.ParseArtworkID(id) -} diff --git a/core/artwork/artwork_test.go b/core/artwork/artwork_test.go index b233e9e68..229f7e0c8 100644 --- a/core/artwork/artwork_test.go +++ b/core/artwork/artwork_test.go @@ -4,12 +4,10 @@ import ( "context" "io" - "github.com/go-chi/jwtauth/v5" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core/artwork" - "github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/resources" "github.com/navidrome/navidrome/tests" @@ -46,31 +44,4 @@ var _ = Describe("Artwork", func() { Expect(result).To(Equal(phBytes)) }) }) - - Context("Public ID Encoding", func() { - BeforeEach(func() { - auth.TokenAuth = jwtauth.New("HS256", []byte("super secret"), nil) - }) - It("returns a reversible string representation", func() { - id := model.NewArtworkID(model.KindArtistArtwork, "1234") - encoded := artwork.EncodeArtworkID(id) - decoded, err := artwork.DecodeArtworkID(encoded) - Expect(err).ToNot(HaveOccurred()) - Expect(decoded).To(Equal(id)) - }) - It("fails to decode an invalid token", func() { - _, err := artwork.DecodeArtworkID("xx-123") - Expect(err).To(MatchError("invalid JWT")) - }) - It("fails to decode an invalid id", func() { - encoded := artwork.EncodeArtworkID(model.ArtworkID{}) - _, err := artwork.DecodeArtworkID(encoded) - Expect(err).To(MatchError("invalid artwork id")) - }) - It("fails to decode a token without an id", func() { - token, _ := auth.CreatePublicToken(map[string]any{}) - _, err := artwork.DecodeArtworkID(token) - Expect(err).To(HaveOccurred()) - }) - }) }) diff --git a/server/public/encode_artwork_id.go b/server/public/encode_artwork_id.go new file mode 100644 index 000000000..1ee39d239 --- /dev/null +++ b/server/public/encode_artwork_id.go @@ -0,0 +1,38 @@ +package public + +import ( + "context" + "errors" + + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/navidrome/navidrome/core/auth" + "github.com/navidrome/navidrome/model" +) + +func EncodeArtworkID(artID model.ArtworkID) string { + token, _ := auth.CreatePublicToken(map[string]any{"id": artID.String()}) + return token +} + +func DecodeArtworkID(tokenString string) (model.ArtworkID, error) { + token, err := auth.TokenAuth.Decode(tokenString) + if err != nil { + return model.ArtworkID{}, err + } + if token == nil { + return model.ArtworkID{}, errors.New("unauthorized") + } + err = jwt.Validate(token, jwt.WithRequiredClaim("id")) + if err != nil { + return model.ArtworkID{}, err + } + claims, err := token.AsMap(context.Background()) + if err != nil { + return model.ArtworkID{}, err + } + id, ok := claims["id"].(string) + if !ok { + return model.ArtworkID{}, errors.New("invalid id type") + } + return model.ParseArtworkID(id) +} diff --git a/server/public/encode_artwork_id_test.go b/server/public/encode_artwork_id_test.go new file mode 100644 index 000000000..a68d1430c --- /dev/null +++ b/server/public/encode_artwork_id_test.go @@ -0,0 +1,39 @@ +package public_test + +import ( + "github.com/go-chi/jwtauth/v5" + "github.com/navidrome/navidrome/core/auth" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/server/public" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("EncodeArtworkID", func() { + Context("Public ID Encoding", func() { + BeforeEach(func() { + auth.TokenAuth = jwtauth.New("HS256", []byte("super secret"), nil) + }) + It("returns a reversible string representation", func() { + id := model.NewArtworkID(model.KindArtistArtwork, "1234") + encoded := public.EncodeArtworkID(id) + decoded, err := public.DecodeArtworkID(encoded) + Expect(err).ToNot(HaveOccurred()) + Expect(decoded).To(Equal(id)) + }) + It("fails to decode an invalid token", func() { + _, err := public.DecodeArtworkID("xx-123") + Expect(err).To(MatchError("invalid JWT")) + }) + It("fails to decode an invalid id", func() { + encoded := public.EncodeArtworkID(model.ArtworkID{}) + _, err := public.DecodeArtworkID(encoded) + Expect(err).To(MatchError("invalid artwork id")) + }) + It("fails to decode a token without an id", func() { + token, _ := auth.CreatePublicToken(map[string]any{}) + _, err := public.DecodeArtworkID(token) + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/server/public/public_endpoints.go b/server/public/public_endpoints.go index 978d61086..b99250c40 100644 --- a/server/public/public_endpoints.go +++ b/server/public/public_endpoints.go @@ -46,7 +46,7 @@ func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) { return } - artId, err := artwork.DecodeArtworkID(id) + artId, err := DecodeArtworkID(id) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/server/public/public_suite_test.go b/server/public/public_suite_test.go new file mode 100644 index 000000000..ea6029f29 --- /dev/null +++ b/server/public/public_suite_test.go @@ -0,0 +1,17 @@ +package public + +import ( + "testing" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPublicEndpoints(t *testing.T) { + tests.Init(t, false) + log.SetLevel(log.LevelFatal) + RegisterFailHandler(Fail) + RunSpecs(t, "Public Endpoints Suite") +} diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index b994549a8..c4d2ab4c2 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -11,10 +11,10 @@ import ( "strings" "github.com/navidrome/navidrome/consts" - "github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server" + "github.com/navidrome/navidrome/server/public" "github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/utils" ) @@ -116,7 +116,7 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 { } func publicImageURL(r *http.Request, artID model.ArtworkID, size int) string { - link := artwork.EncodeArtworkID(artID) + link := public.EncodeArtworkID(artID) path := filepath.Join(consts.URLPathPublicImages, link) params := url.Values{} if size > 0 {