mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor: moved public URL builders to avoid import cycles
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
a78bbca741
commit
66c396413c
70
core/publicurl/publicurl.go
Normal file
70
core/publicurl/publicurl.go
Normal file
@ -0,0 +1,70 @@
|
||||
package publicurl
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
)
|
||||
|
||||
// ImageURL generates a public URL for artwork images.
|
||||
// It creates a signed token for the artwork ID and builds a complete public URL.
|
||||
func ImageURL(req *http.Request, artID model.ArtworkID, size int) string {
|
||||
token, _ := auth.CreatePublicToken(map[string]any{"id": artID.String()})
|
||||
uri := path.Join(consts.URLPathPublicImages, token)
|
||||
params := url.Values{}
|
||||
if size > 0 {
|
||||
params.Add("size", strconv.Itoa(size))
|
||||
}
|
||||
return PublicURL(req, uri, params)
|
||||
}
|
||||
|
||||
// PublicURL builds a full URL for public-facing resources.
|
||||
// It uses ShareURL from config if available, otherwise falls back to extracting
|
||||
// the scheme and host from the provided http.Request.
|
||||
// If req is nil and ShareURL is not set, it defaults to http://localhost.
|
||||
func PublicURL(req *http.Request, u string, params url.Values) string {
|
||||
if conf.Server.ShareURL != "" {
|
||||
shareUrl, _ := url.Parse(conf.Server.ShareURL)
|
||||
buildUrl, _ := url.Parse(u)
|
||||
buildUrl.Scheme = shareUrl.Scheme
|
||||
buildUrl.Host = shareUrl.Host
|
||||
if len(params) > 0 {
|
||||
buildUrl.RawQuery = params.Encode()
|
||||
}
|
||||
return buildUrl.String()
|
||||
}
|
||||
return AbsoluteURL(req, u, params)
|
||||
}
|
||||
|
||||
// AbsoluteURL builds an absolute URL from a relative path.
|
||||
// It uses BaseHost/BaseScheme from config if available, otherwise extracts
|
||||
// the scheme and host from the http.Request.
|
||||
// If req is nil and BaseHost is not set, it defaults to http://localhost.
|
||||
func AbsoluteURL(req *http.Request, u string, params url.Values) string {
|
||||
buildUrl, _ := url.Parse(u)
|
||||
if strings.HasPrefix(u, "/") {
|
||||
buildUrl.Path = path.Join(conf.Server.BasePath, buildUrl.Path)
|
||||
if conf.Server.BaseHost != "" {
|
||||
buildUrl.Scheme = cmp.Or(conf.Server.BaseScheme, "http")
|
||||
buildUrl.Host = conf.Server.BaseHost
|
||||
} else if req != nil {
|
||||
buildUrl.Scheme = req.URL.Scheme
|
||||
buildUrl.Host = req.Host
|
||||
} else {
|
||||
buildUrl.Scheme = "http"
|
||||
buildUrl.Host = "localhost"
|
||||
}
|
||||
}
|
||||
if len(params) > 0 {
|
||||
buildUrl.RawQuery = params.Encode()
|
||||
}
|
||||
return buildUrl.String()
|
||||
}
|
||||
174
core/publicurl/publicurl_test.go
Normal file
174
core/publicurl/publicurl_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
package publicurl_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestPublicURL(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
log.SetLevel(log.LevelFatal)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Public URL Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("Public URL Utilities", func() {
|
||||
BeforeEach(func() {
|
||||
DeferCleanup(configtest.SetupConfig())
|
||||
})
|
||||
|
||||
Describe("PublicURL", func() {
|
||||
When("ShareURL is set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.ShareURL = "https://share.example.com"
|
||||
})
|
||||
|
||||
It("uses ShareURL as the base", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
result := publicurl.PublicURL(r, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("https://share.example.com/path/to/resource"))
|
||||
})
|
||||
|
||||
It("includes query parameters", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
params := url.Values{"size": []string{"300"}, "format": []string{"png"}}
|
||||
result := publicurl.PublicURL(r, "/image/123", params)
|
||||
Expect(result).To(ContainSubstring("https://share.example.com/image/123"))
|
||||
Expect(result).To(ContainSubstring("size=300"))
|
||||
Expect(result).To(ContainSubstring("format=png"))
|
||||
})
|
||||
|
||||
It("works without a request", func() {
|
||||
result := publicurl.PublicURL(nil, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("https://share.example.com/path/to/resource"))
|
||||
})
|
||||
})
|
||||
|
||||
When("ShareURL is not set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.ShareURL = ""
|
||||
})
|
||||
|
||||
It("falls back to AbsoluteURL with request", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/test", nil)
|
||||
r.Host = "myserver.com"
|
||||
result := publicurl.PublicURL(r, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("https://myserver.com/path/to/resource"))
|
||||
})
|
||||
|
||||
It("falls back to localhost without request", func() {
|
||||
result := publicurl.PublicURL(nil, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("http://localhost/path/to/resource"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("AbsoluteURL", func() {
|
||||
When("BaseHost is set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.BaseHost = "configured.example.com"
|
||||
conf.Server.BaseScheme = "https"
|
||||
conf.Server.BasePath = ""
|
||||
})
|
||||
|
||||
It("uses BaseHost and BaseScheme", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
result := publicurl.AbsoluteURL(r, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("https://configured.example.com/path/to/resource"))
|
||||
})
|
||||
|
||||
It("defaults to http scheme if BaseScheme is empty", func() {
|
||||
conf.Server.BaseScheme = ""
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
result := publicurl.AbsoluteURL(r, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("http://configured.example.com/path/to/resource"))
|
||||
})
|
||||
})
|
||||
|
||||
When("BaseHost is not set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.BaseHost = ""
|
||||
conf.Server.BasePath = ""
|
||||
})
|
||||
|
||||
It("extracts host from request", func() {
|
||||
r, _ := http.NewRequest("GET", "https://request.example.com/test", nil)
|
||||
r.Host = "request.example.com"
|
||||
result := publicurl.AbsoluteURL(r, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("https://request.example.com/path/to/resource"))
|
||||
})
|
||||
|
||||
It("falls back to localhost without request", func() {
|
||||
result := publicurl.AbsoluteURL(nil, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("http://localhost/path/to/resource"))
|
||||
})
|
||||
})
|
||||
|
||||
When("BasePath is set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.BasePath = "/navidrome"
|
||||
conf.Server.BaseHost = "example.com"
|
||||
conf.Server.BaseScheme = "https"
|
||||
})
|
||||
|
||||
It("prepends BasePath to the URL", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
result := publicurl.AbsoluteURL(r, "/path/to/resource", nil)
|
||||
Expect(result).To(Equal("https://example.com/navidrome/path/to/resource"))
|
||||
})
|
||||
})
|
||||
|
||||
It("passes through absolute URLs unchanged", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
result := publicurl.AbsoluteURL(r, "https://other.example.com/path", nil)
|
||||
Expect(result).To(Equal("https://other.example.com/path"))
|
||||
})
|
||||
|
||||
It("includes query parameters", func() {
|
||||
conf.Server.BaseHost = "example.com"
|
||||
conf.Server.BaseScheme = "https"
|
||||
r, _ := http.NewRequest("GET", "http://localhost/test", nil)
|
||||
params := url.Values{"key": []string{"value"}}
|
||||
result := publicurl.AbsoluteURL(r, "/path", params)
|
||||
Expect(result).To(Equal("https://example.com/path?key=value"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("ImageURL", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.ShareURL = "https://share.example.com"
|
||||
// Initialize JWT auth for token generation
|
||||
auth.TokenAuth = jwtauth.New("HS256", []byte("test secret"), nil)
|
||||
})
|
||||
|
||||
It("generates a URL with the artwork token", func() {
|
||||
artID := model.NewArtworkID(model.KindAlbumArtwork, "album-123", nil)
|
||||
result := publicurl.ImageURL(nil, artID, 0)
|
||||
Expect(result).To(HavePrefix("https://share.example.com/share/img/"))
|
||||
})
|
||||
|
||||
It("includes size parameter when provided", func() {
|
||||
artID := model.NewArtworkID(model.KindArtistArtwork, "artist-1", nil)
|
||||
result := publicurl.ImageURL(nil, artID, 300)
|
||||
Expect(result).To(ContainSubstring("size=300"))
|
||||
})
|
||||
|
||||
It("omits size parameter when zero", func() {
|
||||
artID := model.NewArtworkID(model.KindMediaFileArtwork, "track-1", nil)
|
||||
result := publicurl.ImageURL(nil, artID, 0)
|
||||
Expect(result).ToNot(ContainSubstring("size="))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,15 +2,8 @@ package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/plugins/host"
|
||||
)
|
||||
@ -23,70 +16,22 @@ func newArtworkService() host.ArtworkService {
|
||||
|
||||
func (a *artworkServiceImpl) GetArtistUrl(_ context.Context, id string, size int32) (string, error) {
|
||||
artID := model.ArtworkID{Kind: model.KindArtistArtwork, ID: id}
|
||||
return a.imageURL(artID, int(size)), nil
|
||||
return publicurl.ImageURL(nil, artID, int(size)), nil
|
||||
}
|
||||
|
||||
func (a *artworkServiceImpl) GetAlbumUrl(_ context.Context, id string, size int32) (string, error) {
|
||||
artID := model.ArtworkID{Kind: model.KindAlbumArtwork, ID: id}
|
||||
return a.imageURL(artID, int(size)), nil
|
||||
return publicurl.ImageURL(nil, artID, int(size)), nil
|
||||
}
|
||||
|
||||
func (a *artworkServiceImpl) GetTrackUrl(_ context.Context, id string, size int32) (string, error) {
|
||||
artID := model.ArtworkID{Kind: model.KindMediaFileArtwork, ID: id}
|
||||
return a.imageURL(artID, int(size)), nil
|
||||
return publicurl.ImageURL(nil, artID, int(size)), nil
|
||||
}
|
||||
|
||||
func (a *artworkServiceImpl) GetPlaylistUrl(_ context.Context, id string, size int32) (string, error) {
|
||||
artID := model.ArtworkID{Kind: model.KindPlaylistArtwork, ID: id}
|
||||
return a.imageURL(artID, int(size)), nil
|
||||
}
|
||||
|
||||
// imageURL generates a public URL for artwork, replicating the logic from server/public
|
||||
// to avoid import cycles.
|
||||
func (a *artworkServiceImpl) imageURL(artID model.ArtworkID, size int) string {
|
||||
token, _ := auth.CreatePublicToken(map[string]any{"id": artID.String()})
|
||||
uri := path.Join(consts.URLPathPublicImages, token)
|
||||
params := url.Values{}
|
||||
if size > 0 {
|
||||
params.Add("size", strconv.Itoa(size))
|
||||
}
|
||||
return a.publicURL(uri, params)
|
||||
}
|
||||
|
||||
// publicURL builds the full URL using ShareURL config or falling back to localhost.
|
||||
func (a *artworkServiceImpl) publicURL(u string, params url.Values) string {
|
||||
var scheme, host string
|
||||
if conf.Server.ShareURL != "" {
|
||||
shareURL, _ := url.Parse(conf.Server.ShareURL)
|
||||
scheme = shareURL.Scheme
|
||||
host = shareURL.Host
|
||||
} else {
|
||||
scheme = "http"
|
||||
host = "localhost"
|
||||
}
|
||||
buildURL, _ := url.Parse(u)
|
||||
buildURL.Scheme = scheme
|
||||
buildURL.Host = host
|
||||
if len(params) > 0 {
|
||||
buildURL.RawQuery = params.Encode()
|
||||
}
|
||||
return buildURL.String()
|
||||
}
|
||||
|
||||
// createRequest creates a dummy HTTP request for URL generation.
|
||||
// Kept for reference but no longer used after refactoring.
|
||||
func (a *artworkServiceImpl) createRequest() *http.Request {
|
||||
var scheme, host string
|
||||
if conf.Server.ShareURL != "" {
|
||||
shareURL, _ := url.Parse(conf.Server.ShareURL)
|
||||
scheme = shareURL.Scheme
|
||||
host = shareURL.Host
|
||||
} else {
|
||||
scheme = "http"
|
||||
host = "localhost"
|
||||
}
|
||||
r, _ := http.NewRequest("GET", fmt.Sprintf("%s://%s", scheme, host), nil)
|
||||
return r
|
||||
return publicurl.ImageURL(nil, artID, int(size)), nil
|
||||
}
|
||||
|
||||
var _ host.ArtworkService = (*artworkServiceImpl)(nil)
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
)
|
||||
|
||||
func ImageURL(r *http.Request, artID model.ArtworkID, size int) string {
|
||||
token := encodeArtworkID(artID)
|
||||
uri := path.Join(consts.URLPathPublicImages, token)
|
||||
params := url.Values{}
|
||||
if size > 0 {
|
||||
params.Add("size", strconv.Itoa(size))
|
||||
}
|
||||
return publicURL(r, uri, params)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
artID, err := model.ParseArtworkID(id)
|
||||
if err == nil {
|
||||
return artID, nil
|
||||
}
|
||||
// Try to default to mediafile artworkId (if used with a mediafileShare token)
|
||||
return model.ParseArtworkID("mf-" + id)
|
||||
}
|
||||
|
||||
func encodeMediafileShare(s model.Share, id string) string {
|
||||
claims := map[string]any{"id": id}
|
||||
if s.Format != "" {
|
||||
claims["f"] = s.Format
|
||||
}
|
||||
if s.MaxBitRate != 0 {
|
||||
claims["b"] = s.MaxBitRate
|
||||
}
|
||||
token, _ := auth.CreateExpiringPublicToken(V(s.ExpiresAt), claims)
|
||||
return token
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
. "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", nil)
|
||||
encoded := encodeArtworkID(id)
|
||||
decoded, err := decodeArtworkID(encoded)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(decoded).To(Equal(id))
|
||||
})
|
||||
It("fails to decode an invalid token", func() {
|
||||
_, err := decodeArtworkID("xx-123")
|
||||
Expect(err).To(MatchError("invalid JWT"))
|
||||
})
|
||||
It("defaults to kind mediafile", func() {
|
||||
encoded := encodeArtworkID(model.ArtworkID{})
|
||||
id, err := decodeArtworkID(encoded)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(id.Kind).To(Equal(model.KindMediaFileArtwork))
|
||||
})
|
||||
It("fails to decode a token without an id", func() {
|
||||
token, _ := auth.CreatePublicToken(map[string]any{})
|
||||
_, err := decodeArtworkID(token)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -7,7 +7,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
@ -65,3 +67,31 @@ func (pub *Router) handleImages(w http.ResponseWriter, r *http.Request) {
|
||||
log.Warn(ctx, "Error sending image", "count", cnt, err)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
artID, err := model.ParseArtworkID(id)
|
||||
if err == nil {
|
||||
return artID, nil
|
||||
}
|
||||
// Try to default to mediafile artworkId (if used with a mediafileShare token)
|
||||
return model.ParseArtworkID("mf-" + id)
|
||||
}
|
||||
|
||||
33
server/public/handle_images_test.go
Normal file
33
server/public/handle_images_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("decodeArtworkID", func() {
|
||||
BeforeEach(func() {
|
||||
auth.TokenAuth = jwtauth.New("HS256", []byte("super secret"), nil)
|
||||
})
|
||||
|
||||
It("fails to decode an invalid token", func() {
|
||||
_, err := decodeArtworkID("xx-123")
|
||||
Expect(err).To(MatchError("invalid JWT"))
|
||||
})
|
||||
|
||||
It("defaults to kind mediafile for empty artwork ID", func() {
|
||||
token, _ := auth.CreatePublicToken(map[string]any{"id": ""})
|
||||
id, err := decodeArtworkID(token)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(id.Kind).To(Equal(model.KindMediaFileArtwork))
|
||||
})
|
||||
|
||||
It("fails to decode a token without an id", func() {
|
||||
token, _ := auth.CreatePublicToken(map[string]any{})
|
||||
_, err := decodeArtworkID(token)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
@ -7,10 +7,13 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/ui"
|
||||
. "github.com/navidrome/navidrome/utils/gg"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
@ -78,7 +81,7 @@ func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id s
|
||||
|
||||
func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
|
||||
s.URL = ShareURL(r, s.ID)
|
||||
s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
|
||||
s.ImageURL = publicurl.ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
|
||||
for i := range s.Tracks {
|
||||
s.Tracks[i].ID = encodeMediafileShare(s, s.Tracks[i].ID)
|
||||
}
|
||||
@ -88,7 +91,19 @@ func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
|
||||
func (pub *Router) mapShareToM3U(r *http.Request, s model.Share) *model.Share {
|
||||
for i := range s.Tracks {
|
||||
id := encodeMediafileShare(s, s.Tracks[i].ID)
|
||||
s.Tracks[i].Path = publicURL(r, path.Join(consts.URLPathPublic, "s", id), nil)
|
||||
s.Tracks[i].Path = publicurl.PublicURL(r, path.Join(consts.URLPathPublic, "s", id), nil)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func encodeMediafileShare(s model.Share, id string) string {
|
||||
claims := map[string]any{"id": id}
|
||||
if s.Format != "" {
|
||||
claims["f"] = s.Format
|
||||
}
|
||||
if s.MaxBitRate != 0 {
|
||||
claims["b"] = s.MaxBitRate
|
||||
}
|
||||
token, _ := auth.CreateExpiringPublicToken(V(s.ExpiresAt), claims)
|
||||
return token
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package public
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -11,6 +10,7 @@ import (
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
@ -67,19 +67,5 @@ func (pub *Router) routes() http.Handler {
|
||||
|
||||
func ShareURL(r *http.Request, id string) string {
|
||||
uri := path.Join(consts.URLPathPublic, id)
|
||||
return publicURL(r, uri, nil)
|
||||
}
|
||||
|
||||
func publicURL(r *http.Request, u string, params url.Values) string {
|
||||
if conf.Server.ShareURL != "" {
|
||||
shareUrl, _ := url.Parse(conf.Server.ShareURL)
|
||||
buildUrl, _ := url.Parse(u)
|
||||
buildUrl.Scheme = shareUrl.Scheme
|
||||
buildUrl.Host = shareUrl.Host
|
||||
if len(params) > 0 {
|
||||
buildUrl.RawQuery = params.Encode()
|
||||
}
|
||||
return buildUrl.String()
|
||||
}
|
||||
return server.AbsoluteURL(r, u, params)
|
||||
return publicurl.PublicURL(r, uri, nil)
|
||||
}
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("publicURL", func() {
|
||||
When("ShareURL is set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.ShareURL = "http://share.myotherserver.com"
|
||||
})
|
||||
It("uses the config value instead of AbsoluteURL", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/share/123", nil)
|
||||
uri := path.Join(consts.URLPathPublic, "123")
|
||||
actual := publicURL(r, uri, nil)
|
||||
Expect(actual).To(Equal("http://share.myotherserver.com/share/123"))
|
||||
})
|
||||
It("concatenates params if provided", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/share/123", nil)
|
||||
uri := path.Join(consts.URLPathPublicImages, "123")
|
||||
params := url.Values{
|
||||
"size": []string{"300"},
|
||||
}
|
||||
actual := publicURL(r, uri, params)
|
||||
Expect(actual).To(Equal("http://share.myotherserver.com/share/img/123?size=300"))
|
||||
|
||||
})
|
||||
})
|
||||
When("ShareURL is not set", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.ShareURL = ""
|
||||
})
|
||||
It("uses AbsoluteURL", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/share/123", nil)
|
||||
uri := path.Join(consts.URLPathPublic, "123")
|
||||
actual := publicURL(r, uri, nil)
|
||||
Expect(actual).To(Equal("https://myserver.com/share/123"))
|
||||
})
|
||||
It("concatenates params if provided", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/share/123", nil)
|
||||
uri := path.Join(consts.URLPathPublicImages, "123")
|
||||
params := url.Values{
|
||||
"size": []string{"300"},
|
||||
}
|
||||
actual := publicURL(r, uri, params)
|
||||
Expect(actual).To(Equal("https://myserver.com/share/img/123?size=300"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,7 +2,6 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
@ -10,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
@ -242,24 +240,6 @@ func (s *Server) frontendAssetsHandler() http.Handler {
|
||||
return r
|
||||
}
|
||||
|
||||
func AbsoluteURL(r *http.Request, u string, params url.Values) string {
|
||||
buildUrl, _ := url.Parse(u)
|
||||
if strings.HasPrefix(u, "/") {
|
||||
buildUrl.Path = path.Join(conf.Server.BasePath, buildUrl.Path)
|
||||
if conf.Server.BaseHost != "" {
|
||||
buildUrl.Scheme = cmp.Or(conf.Server.BaseScheme, "http")
|
||||
buildUrl.Host = conf.Server.BaseHost
|
||||
} else {
|
||||
buildUrl.Scheme = r.URL.Scheme
|
||||
buildUrl.Host = r.Host
|
||||
}
|
||||
}
|
||||
if len(params) > 0 {
|
||||
buildUrl.RawQuery = params.Encode()
|
||||
}
|
||||
return buildUrl.String()
|
||||
}
|
||||
|
||||
// validateTLSCertificates validates the TLS certificate and key files before starting the server.
|
||||
// It provides detailed error messages for common issues like encrypted private keys.
|
||||
func validateTLSCertificates(certFile, keyFile string) error {
|
||||
|
||||
@ -7,68 +7,16 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("AbsoluteURL", func() {
|
||||
When("BaseURL is empty", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.BasePath = ""
|
||||
})
|
||||
It("uses the scheme/host from the request", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/rest/ping?id=123", nil)
|
||||
actual := AbsoluteURL(r, "/share/img/123", url.Values{"a": []string{"xyz"}})
|
||||
Expect(actual).To(Equal("https://myserver.com/share/img/123?a=xyz"))
|
||||
})
|
||||
It("does not override provided schema/host", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/rest/ping?id=123", nil)
|
||||
actual := AbsoluteURL(r, "http://public.myserver.com/share/img/123", url.Values{"a": []string{"xyz"}})
|
||||
Expect(actual).To(Equal("http://public.myserver.com/share/img/123?a=xyz"))
|
||||
})
|
||||
})
|
||||
When("BaseURL has only path", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.BasePath = "/music"
|
||||
})
|
||||
It("uses the scheme/host from the request", func() {
|
||||
r, _ := http.NewRequest("GET", "https://myserver.com/rest/ping?id=123", nil)
|
||||
actual := AbsoluteURL(r, "/share/img/123", url.Values{"a": []string{"xyz"}})
|
||||
Expect(actual).To(Equal("https://myserver.com/music/share/img/123?a=xyz"))
|
||||
})
|
||||
It("does not override provided schema/host", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/rest/ping?id=123", nil)
|
||||
actual := AbsoluteURL(r, "http://public.myserver.com/share/img/123", url.Values{"a": []string{"xyz"}})
|
||||
Expect(actual).To(Equal("http://public.myserver.com/share/img/123?a=xyz"))
|
||||
})
|
||||
})
|
||||
When("BaseURL has full URL", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.BaseScheme = "https"
|
||||
conf.Server.BaseHost = "myserver.com:8080"
|
||||
conf.Server.BasePath = "/music"
|
||||
})
|
||||
It("use the configured scheme/host/path", func() {
|
||||
r, _ := http.NewRequest("GET", "https://localhost:4533/rest/ping?id=123", nil)
|
||||
actual := AbsoluteURL(r, "/share/img/123", url.Values{"a": []string{"xyz"}})
|
||||
Expect(actual).To(Equal("https://myserver.com:8080/music/share/img/123?a=xyz"))
|
||||
})
|
||||
It("does not override provided schema/host", func() {
|
||||
r, _ := http.NewRequest("GET", "http://localhost/rest/ping?id=123", nil)
|
||||
actual := AbsoluteURL(r, "http://public.myserver.com/share/img/123", url.Values{"a": []string{"xyz"}})
|
||||
Expect(actual).To(Equal("http://public.myserver.com/share/img/123?a=xyz"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("createUnixSocketFile", func() {
|
||||
var socketPath string
|
||||
|
||||
|
||||
@ -8,9 +8,9 @@ import (
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/filter"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
@ -230,9 +230,9 @@ func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) {
|
||||
response := newResponse()
|
||||
response.AlbumInfo = &responses.AlbumInfo{}
|
||||
response.AlbumInfo.Notes = album.Description
|
||||
response.AlbumInfo.SmallImageUrl = public.ImageURL(r, album.CoverArtID(), 300)
|
||||
response.AlbumInfo.MediumImageUrl = public.ImageURL(r, album.CoverArtID(), 600)
|
||||
response.AlbumInfo.LargeImageUrl = public.ImageURL(r, album.CoverArtID(), 1200)
|
||||
response.AlbumInfo.SmallImageUrl = publicurl.ImageURL(r, album.CoverArtID(), 300)
|
||||
response.AlbumInfo.MediumImageUrl = publicurl.ImageURL(r, album.CoverArtID(), 600)
|
||||
response.AlbumInfo.LargeImageUrl = publicurl.ImageURL(r, album.CoverArtID(), 1200)
|
||||
|
||||
response.AlbumInfo.LastFmUrl = album.ExternalUrl
|
||||
response.AlbumInfo.MusicBrainzID = album.MbzAlbumID
|
||||
@ -296,9 +296,9 @@ func (api *Router) getArtistInfo(r *http.Request) (*responses.ArtistInfoBase, *m
|
||||
|
||||
base := responses.ArtistInfoBase{}
|
||||
base.Biography = artist.Biography
|
||||
base.SmallImageUrl = public.ImageURL(r, artist.CoverArtID(), 300)
|
||||
base.MediumImageUrl = public.ImageURL(r, artist.CoverArtID(), 600)
|
||||
base.LargeImageUrl = public.ImageURL(r, artist.CoverArtID(), 1200)
|
||||
base.SmallImageUrl = publicurl.ImageURL(r, artist.CoverArtID(), 300)
|
||||
base.MediumImageUrl = publicurl.ImageURL(r, artist.CoverArtID(), 600)
|
||||
base.LargeImageUrl = publicurl.ImageURL(r, artist.CoverArtID(), 1200)
|
||||
base.LastFmUrl = artist.ExternalUrl
|
||||
base.MusicBrainzID = artist.MbzArtistID
|
||||
|
||||
|
||||
@ -13,9 +13,9 @@ import (
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/number"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
@ -99,7 +99,7 @@ func toArtist(r *http.Request, a model.Artist) responses.Artist {
|
||||
Name: a.Name,
|
||||
UserRating: int32(a.Rating),
|
||||
CoverArt: a.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600),
|
||||
ArtistImageUrl: publicurl.ImageURL(r, a.CoverArtID(), 600),
|
||||
}
|
||||
if a.Starred {
|
||||
artist.Starred = a.StarredAt
|
||||
@ -113,7 +113,7 @@ func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 {
|
||||
Name: a.Name,
|
||||
AlbumCount: getArtistAlbumCount(&a),
|
||||
CoverArt: a.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600),
|
||||
ArtistImageUrl: publicurl.ImageURL(r, a.CoverArtID(), 600),
|
||||
UserRating: int32(a.Rating),
|
||||
}
|
||||
if a.Starred {
|
||||
|
||||
@ -10,9 +10,9 @@ import (
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/sanitize"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/public"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
"github.com/navidrome/navidrome/utils/slice"
|
||||
@ -119,7 +119,7 @@ func (api *Router) Search2(r *http.Request) (*responses.Subsonic, error) {
|
||||
Name: artist.Name,
|
||||
UserRating: int32(artist.Rating),
|
||||
CoverArt: artist.CoverArtID().String(),
|
||||
ArtistImageUrl: public.ImageURL(r, artist.CoverArtID(), 600),
|
||||
ArtistImageUrl: publicurl.ImageURL(r, artist.CoverArtID(), 600),
|
||||
}
|
||||
if artist.Starred {
|
||||
a.Starred = artist.StarredAt
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user