mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-09 19:07:12 +02:00
POST /api/player passed the ownership check using the userId from the request body, then saved with the body id. When that id belonged to another user's player, the save became an update with no owner restriction, overwriting the row and moving it to the caller. Save now always creates a new player and ignores any id in the body; edits keep going through the owner-scoped Update. Player registration also reused a player by the id sent in the Subsonic player cookie or the Jellyfin DeviceId without checking its owner, letting a user attach to another user's player and overwrite its name, user agent and IP. Register now only reuses a player owned by the requesting user, falling back to the user's own players otherwise.
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/id"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/utils"
|
|
)
|
|
|
|
type Players interface {
|
|
Get(ctx context.Context, playerId string) (*model.Player, error)
|
|
Register(ctx context.Context, id, client, userAgent, ip string) (*model.Player, *model.Transcoding, error)
|
|
}
|
|
|
|
func NewPlayers(ds model.DataStore) Players {
|
|
return &players{
|
|
ds: ds,
|
|
limiter: utils.Limiter{Interval: consts.UpdatePlayerFrequency},
|
|
}
|
|
}
|
|
|
|
type players struct {
|
|
ds model.DataStore
|
|
limiter utils.Limiter
|
|
}
|
|
|
|
func (p *players) Register(ctx context.Context, playerID, client, userAgent, ip string) (*model.Player, *model.Transcoding, error) {
|
|
var plr *model.Player
|
|
var trc *model.Transcoding
|
|
var err error
|
|
user, _ := request.UserFrom(ctx)
|
|
if playerID != "" {
|
|
plr, err = p.ds.Player(ctx).Get(playerID)
|
|
if err == nil && (plr.Client != client || plr.UserId != user.ID) {
|
|
playerID = ""
|
|
}
|
|
}
|
|
username := userName(ctx)
|
|
if err != nil || playerID == "" {
|
|
plr, err = p.ds.Player(ctx).FindMatch(user.ID, client, userAgent)
|
|
if err == nil {
|
|
log.Debug(ctx, "Found matching player", "id", plr.ID, "client", client, "username", username, "type", userAgent)
|
|
} else {
|
|
plr = &model.Player{
|
|
ID: id.NewRandom(),
|
|
UserId: user.ID,
|
|
Client: client,
|
|
ScrobbleEnabled: true,
|
|
ReportRealPath: conf.Server.Subsonic.DefaultReportRealPath,
|
|
}
|
|
log.Info(ctx, "Registering new player", "id", plr.ID, "client", client, "username", username, "type", userAgent)
|
|
}
|
|
}
|
|
plr.Name = fmt.Sprintf("%s [%s]", client, userAgent)
|
|
plr.UserAgent = userAgent
|
|
plr.IP = ip
|
|
plr.LastSeen = time.Now()
|
|
p.limiter.Do(plr.ID, func() {
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
|
defer cancel()
|
|
|
|
err = p.ds.Player(ctx).Put(plr)
|
|
if err != nil {
|
|
log.Warn(ctx, "Could not save player", "id", plr.ID, "client", client, "username", username, "type", userAgent, err)
|
|
}
|
|
})
|
|
if plr.TranscodingId != "" {
|
|
trc, err = p.ds.Transcoding(ctx).Get(plr.TranscodingId)
|
|
}
|
|
return plr, trc, err
|
|
}
|
|
|
|
func (p *players) Get(ctx context.Context, playerId string) (*model.Player, error) {
|
|
return p.ds.Player(ctx).Get(playerId)
|
|
}
|