mirror of
https://github.com/navidrome/navidrome.git
synced 2026-06-19 07:37:15 +00:00
3b958dd6a7
The jwx library always deserializes numeric claims from a parsed token as float64, so the int and int64 branches in getIntClaim could never succeed and were dead code. Keep only the float64 path, which is the one actually exercised by the token round-trip, and update the comment to document the library behavior.
142 lines
3.8 KiB
Go
142 lines
3.8 KiB
Go
package stream
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/lestrrat-go/jwx/v3/jwt"
|
|
"github.com/navidrome/navidrome/core/auth"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
const tokenTTL = 48 * time.Hour
|
|
|
|
// params contains the parameters extracted from a transcode token.
|
|
// TargetBitrate is in kilobits per second (kbps).
|
|
type params struct {
|
|
MediaID string
|
|
DirectPlay bool
|
|
TargetFormat string
|
|
TargetBitrate int
|
|
TargetChannels int
|
|
TargetSampleRate int
|
|
TargetBitDepth int
|
|
SourceUpdatedAt time.Time
|
|
}
|
|
|
|
// toClaimsMap converts a Decision into a JWT claims map for token encoding.
|
|
// Only non-zero transcode fields are included.
|
|
func (d *TranscodeDecision) toClaimsMap() map[string]any {
|
|
m := map[string]any{
|
|
"mid": d.MediaID,
|
|
"ua": d.SourceUpdatedAt.Truncate(time.Second).Unix(),
|
|
jwt.ExpirationKey: time.Now().Add(tokenTTL).UTC().Unix(),
|
|
}
|
|
if d.CanDirectPlay {
|
|
m["dp"] = true
|
|
}
|
|
if d.CanTranscode && d.TargetFormat != "" {
|
|
m["f"] = d.TargetFormat
|
|
if d.TargetBitrate != 0 {
|
|
m["b"] = d.TargetBitrate
|
|
}
|
|
if d.TargetChannels != 0 {
|
|
m["ch"] = d.TargetChannels
|
|
}
|
|
if d.TargetSampleRate != 0 {
|
|
m["sr"] = d.TargetSampleRate
|
|
}
|
|
if d.TargetBitDepth != 0 {
|
|
m["bd"] = d.TargetBitDepth
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// paramsFromToken extracts and validates Params from a parsed JWT token.
|
|
// Returns an error if required claims (media ID, source timestamp) are missing.
|
|
func paramsFromToken(token jwt.Token) (*params, error) {
|
|
var p params
|
|
var mid string
|
|
if err := token.Get("mid", &mid); err == nil {
|
|
p.MediaID = mid
|
|
}
|
|
if p.MediaID == "" {
|
|
return nil, fmt.Errorf("%w: missing media ID", ErrTokenInvalid)
|
|
}
|
|
|
|
var dp bool
|
|
if err := token.Get("dp", &dp); err == nil {
|
|
p.DirectPlay = dp
|
|
}
|
|
|
|
ua := getIntClaim(token, "ua")
|
|
if ua != 0 {
|
|
p.SourceUpdatedAt = time.Unix(int64(ua), 0)
|
|
}
|
|
if p.SourceUpdatedAt.IsZero() {
|
|
return nil, fmt.Errorf("%w: missing source timestamp", ErrTokenInvalid)
|
|
}
|
|
|
|
var f string
|
|
if err := token.Get("f", &f); err == nil {
|
|
p.TargetFormat = f
|
|
}
|
|
p.TargetBitrate = getIntClaim(token, "b")
|
|
p.TargetChannels = getIntClaim(token, "ch")
|
|
p.TargetSampleRate = getIntClaim(token, "sr")
|
|
p.TargetBitDepth = getIntClaim(token, "bd")
|
|
return &p, nil
|
|
}
|
|
|
|
// getIntClaim extracts a numeric claim from a JWT token. Numeric claims in a
|
|
// parsed token are always deserialized as float64, regardless of the type used
|
|
// when encoding.
|
|
func getIntClaim(token jwt.Token, key string) int {
|
|
var f float64
|
|
if err := token.Get(key, &f); err == nil {
|
|
return int(f)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *deciderService) CreateTranscodeParams(decision *TranscodeDecision) (string, error) {
|
|
return auth.EncodeToken(decision.toClaimsMap())
|
|
}
|
|
|
|
func (s *deciderService) parseTranscodeParams(tokenStr string) (*params, error) {
|
|
token, err := auth.DecodeAndVerifyToken(tokenStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return paramsFromToken(token)
|
|
}
|
|
|
|
func (s *deciderService) ResolveRequestFromToken(ctx context.Context, token string, mf *model.MediaFile, offset int) (Request, error) {
|
|
p, err := s.parseTranscodeParams(token)
|
|
if err != nil {
|
|
return Request{}, errors.Join(ErrTokenInvalid, err)
|
|
}
|
|
if p.MediaID != mf.ID {
|
|
return Request{}, fmt.Errorf("%w: token mediaID %q does not match %q", ErrTokenInvalid, p.MediaID, mf.ID)
|
|
}
|
|
if !mf.UpdatedAt.Truncate(time.Second).Equal(p.SourceUpdatedAt) {
|
|
log.Info(ctx, "Transcode token is stale", "mediaID", mf.ID,
|
|
"tokenUpdatedAt", p.SourceUpdatedAt, "fileUpdatedAt", mf.UpdatedAt)
|
|
return Request{}, ErrTokenStale
|
|
}
|
|
|
|
req := Request{Offset: offset}
|
|
if !p.DirectPlay && p.TargetFormat != "" {
|
|
req.Format = p.TargetFormat
|
|
req.BitRate = p.TargetBitrate
|
|
req.SampleRate = p.TargetSampleRate
|
|
req.BitDepth = p.TargetBitDepth
|
|
req.Channels = p.TargetChannels
|
|
}
|
|
return req, nil
|
|
}
|