navidrome/server/public/handle_shares.go
Deluan 82f9f88c0f
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
refactor(auth): replace untyped JWT claims with typed Claims struct
Introduced a typed Claims struct in core/auth to replace the raw
map[string]any approach used for JWT claims throughout the codebase.
This provides compile-time safety and better readability when creating,
validating, and extracting JWT tokens. Also upgraded lestrrat-go/jwx
from v2 to v3 and go-chi/jwtauth to v5.4.0, adapting all callers to
the new API where token accessor methods now return tuples instead of
bare values. Updated all affected handlers, middleware, and tests.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 14:03:27 -05:00

107 lines
3 KiB
Go

package public
import (
"context"
"errors"
"net/http"
"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"
)
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())) //nolint:gosec
}
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 = publicurl.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.PublicURL(r, path.Join(consts.URLPathPublic, "s", id), nil)
}
return &s
}
func encodeMediafileShare(s model.Share, id string) string {
claims := auth.Claims{
ID: id,
Format: s.Format,
BitRate: s.MaxBitRate,
}
token, _ := auth.CreateExpiringPublicToken(V(s.ExpiresAt), claims)
return token
}