update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
This commit is contained in:
71
server/public/encode_id.go
Normal file
71
server/public/encode_id.go
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
||||
39
server/public/encode_id_test.go
Normal file
39
server/public/encode_id_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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())
|
||||
})
|
||||
})
|
||||
})
|
||||
18
server/public/handle_downloads.go
Normal file
18
server/public/handle_downloads.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (pub *Router) handleDownloads(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := req.Params(r).String(":id")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = pub.archiver.ZipShare(r.Context(), id, w)
|
||||
checkShareError(r.Context(), w, err, id)
|
||||
}
|
||||
67
server/public/handle_images.go
Normal file
67
server/public/handle_images.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (pub *Router) handleImages(w http.ResponseWriter, r *http.Request) {
|
||||
// If context is already canceled, discard request without further processing
|
||||
if r.Context().Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
p := req.Params(r)
|
||||
id, _ := p.String(":id")
|
||||
if id == "" {
|
||||
log.Warn(r, "No id provided")
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
artId, err := decodeArtworkID(id)
|
||||
if err != nil {
|
||||
log.Error(r, "Error decoding artwork id", "id", id, err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
size := p.IntOr("size", 0)
|
||||
square := p.BoolOr("square", false)
|
||||
|
||||
imgReader, lastUpdate, err := pub.artwork.Get(ctx, artId, size, square)
|
||||
switch {
|
||||
case errors.Is(err, context.Canceled):
|
||||
return
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
log.Warn(r, "Couldn't find coverArt", "id", id, err)
|
||||
http.Error(w, "Artwork not found", http.StatusNotFound)
|
||||
return
|
||||
case errors.Is(err, artwork.ErrUnavailable):
|
||||
log.Debug(r, "Item does not have artwork", "id", id, err)
|
||||
http.Error(w, "Artwork not found", http.StatusNotFound)
|
||||
return
|
||||
case err != nil:
|
||||
log.Error(r, "Error retrieving coverArt", "id", id, err)
|
||||
http.Error(w, "Error retrieving coverArt", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defer imgReader.Close()
|
||||
w.Header().Set("Cache-Control", "public, max-age=315360000")
|
||||
w.Header().Set("Last-Modified", lastUpdate.Format(time.RFC1123))
|
||||
cnt, err := io.Copy(w, imgReader)
|
||||
if err != nil {
|
||||
log.Warn(ctx, "Error sending image", "count", cnt, err)
|
||||
}
|
||||
}
|
||||
94
server/public/handle_shares.go
Normal file
94
server/public/handle_shares.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"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/req"
|
||||
)
|
||||
|
||||
func (pub *Router) handleShares(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := req.Params(r).String(":id")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// If requested file is a UI asset, just serve it
|
||||
_, err = ui.BuildAssets().Open(id)
|
||||
if err == nil {
|
||||
pub.assetsHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// If it is not, consider it a share ID
|
||||
s, err := pub.share.Load(r.Context(), id)
|
||||
if err != nil {
|
||||
checkShareError(r.Context(), w, err, id)
|
||||
return
|
||||
}
|
||||
|
||||
s = pub.mapShareInfo(r, *s)
|
||||
server.IndexWithShare(pub.ds, ui.BuildAssets(), s)(w, r)
|
||||
}
|
||||
|
||||
func (pub *Router) handleM3U(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := req.Params(r).String(":id")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// If it is not, consider it a share ID
|
||||
s, err := pub.share.Load(r.Context(), id)
|
||||
if err != nil {
|
||||
checkShareError(r.Context(), w, err, id)
|
||||
return
|
||||
}
|
||||
|
||||
s = pub.mapShareToM3U(r, *s)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||
_, _ = w.Write([]byte(s.ToM3U8()))
|
||||
}
|
||||
|
||||
func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id string) {
|
||||
switch {
|
||||
case errors.Is(err, model.ErrExpired):
|
||||
log.Error(ctx, "Share expired", "id", id, err)
|
||||
http.Error(w, "Share not available anymore", http.StatusGone)
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
log.Error(ctx, "Share not found", "id", id, err)
|
||||
http.Error(w, "Share not found", http.StatusNotFound)
|
||||
case errors.Is(err, model.ErrNotAuthorized):
|
||||
log.Error(ctx, "Share is not downloadable", "id", id, err)
|
||||
http.Error(w, "This share is not downloadable", http.StatusForbidden)
|
||||
case err != nil:
|
||||
log.Error(ctx, "Error retrieving share", "id", id, err)
|
||||
http.Error(w, "Error retrieving share", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
for i := range s.Tracks {
|
||||
s.Tracks[i].ID = encodeMediafileShare(s, s.Tracks[i].ID)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
105
server/public/handle_streams.go
Normal file
105
server/public/handle_streams.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
p := req.Params(r)
|
||||
tokenId, _ := p.String(":id")
|
||||
info, err := decodeStreamInfo(tokenId)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error parsing shared stream info", err)
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := pub.streamer.NewStream(ctx, info.id, info.format, info.bitrate, 0)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error starting shared stream", err)
|
||||
http.Error(w, "invalid request", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Make sure the stream will be closed at the end, to avoid leakage
|
||||
defer func() {
|
||||
if err := stream.Close(); err != nil && log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
log.Error("Error closing shared stream", "id", info.id, "file", stream.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Content-Duration", strconv.FormatFloat(float64(stream.Duration()), 'G', -1, 32))
|
||||
|
||||
if stream.Seekable() {
|
||||
http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream)
|
||||
} else {
|
||||
// If the stream doesn't provide a size (i.e. is not seekable), we can't support ranges/content-length
|
||||
w.Header().Set("Accept-Ranges", "none")
|
||||
w.Header().Set("Content-Type", stream.ContentType())
|
||||
|
||||
estimateContentLength := p.BoolOr("estimateContentLength", false)
|
||||
|
||||
// if Client requests the estimated content-length, send it
|
||||
if estimateContentLength {
|
||||
length := strconv.Itoa(stream.EstimatedContentLength())
|
||||
log.Trace(ctx, "Estimated content-length", "contentLength", length)
|
||||
w.Header().Set("Content-Length", length)
|
||||
}
|
||||
|
||||
if r.Method == http.MethodHead {
|
||||
go func() { _, _ = io.Copy(io.Discard, stream) }()
|
||||
} else {
|
||||
c, err := io.Copy(w, stream)
|
||||
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending shared transcoded file", "id", info.id, err)
|
||||
} else {
|
||||
log.Trace(ctx, "Success sending shared transcode file", "id", info.id, "size", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type shareTrackInfo struct {
|
||||
id string
|
||||
format string
|
||||
bitrate int
|
||||
}
|
||||
|
||||
func decodeStreamInfo(tokenString string) (shareTrackInfo, error) {
|
||||
token, err := auth.TokenAuth.Decode(tokenString)
|
||||
if err != nil {
|
||||
return shareTrackInfo{}, err
|
||||
}
|
||||
if token == nil {
|
||||
return shareTrackInfo{}, errors.New("unauthorized")
|
||||
}
|
||||
err = jwt.Validate(token, jwt.WithRequiredClaim("id"))
|
||||
if err != nil {
|
||||
return shareTrackInfo{}, err
|
||||
}
|
||||
claims, err := token.AsMap(context.Background())
|
||||
if err != nil {
|
||||
return shareTrackInfo{}, err
|
||||
}
|
||||
id, ok := claims["id"].(string)
|
||||
if !ok {
|
||||
return shareTrackInfo{}, errors.New("invalid id type")
|
||||
}
|
||||
resp := shareTrackInfo{}
|
||||
resp.id = id
|
||||
resp.format, _ = claims["f"].(string)
|
||||
resp.bitrate, _ = claims["b"].(int)
|
||||
return resp, nil
|
||||
}
|
||||
85
server/public/public.go
Normal file
85
server/public/public.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
"github.com/navidrome/navidrome/ui"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
http.Handler
|
||||
artwork artwork.Artwork
|
||||
streamer core.MediaStreamer
|
||||
archiver core.Archiver
|
||||
share core.Share
|
||||
assetsHandler http.Handler
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, share core.Share, archiver core.Archiver) *Router {
|
||||
p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share, archiver: archiver}
|
||||
shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic)
|
||||
p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
|
||||
p.Handler = p.routes()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (pub *Router) routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(server.URLParamsMiddleware)
|
||||
r.Group(func(r chi.Router) {
|
||||
if conf.Server.DevArtworkMaxRequests > 0 {
|
||||
log.Debug("Throttling public images endpoint", "maxRequests", conf.Server.DevArtworkMaxRequests,
|
||||
"backlogLimit", conf.Server.DevArtworkThrottleBacklogLimit, "backlogTimeout",
|
||||
conf.Server.DevArtworkThrottleBacklogTimeout)
|
||||
r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit,
|
||||
conf.Server.DevArtworkThrottleBacklogTimeout))
|
||||
}
|
||||
r.HandleFunc("/img/{id}", pub.handleImages)
|
||||
})
|
||||
if conf.Server.EnableSharing {
|
||||
r.HandleFunc("/s/{id}", pub.handleStream)
|
||||
if conf.Server.EnableDownloads {
|
||||
r.HandleFunc("/d/{id}", pub.handleDownloads)
|
||||
}
|
||||
r.HandleFunc("/{id}/m3u", pub.handleM3U)
|
||||
r.HandleFunc("/{id}", pub.handleShares)
|
||||
r.HandleFunc("/", pub.handleShares)
|
||||
r.Handle("/*", pub.assetsHandler)
|
||||
}
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
17
server/public/public_suite_test.go
Normal file
17
server/public/public_suite_test.go
Normal file
@@ -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")
|
||||
}
|
||||
56
server/public/public_test.go
Normal file
56
server/public/public_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user