mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-11 03:47:18 +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.
167 lines
5.8 KiB
Go
167 lines
5.8 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Players", func() {
|
|
var players Players
|
|
var repo *mockPlayerRepository
|
|
ctx := log.NewContext(context.TODO())
|
|
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "johndoe"})
|
|
ctx = request.WithUsername(ctx, "johndoe")
|
|
var beforeRegister time.Time
|
|
|
|
BeforeEach(func() {
|
|
repo = &mockPlayerRepository{}
|
|
ds := &tests.MockDataStore{MockedPlayer: repo, MockedTranscoding: &tests.MockTranscodingRepo{}}
|
|
players = NewPlayers(ds)
|
|
beforeRegister = time.Now()
|
|
})
|
|
|
|
Describe("Register", func() {
|
|
It("creates a new player when no ID is specified", func() {
|
|
p, trc, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).ToNot(BeEmpty())
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(p.Client).To(Equal("client"))
|
|
Expect(p.UserId).To(Equal("userid"))
|
|
Expect(p.UserAgent).To(Equal("chrome"))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
Expect(trc).To(BeNil())
|
|
})
|
|
|
|
It("creates a new player if it cannot find any matching player", func() {
|
|
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).ToNot(BeEmpty())
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
Expect(trc).To(BeNil())
|
|
})
|
|
|
|
It("creates a new player if client does not match the one in DB", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client1111", LastSeen: time.Time{}}
|
|
repo.add(plr)
|
|
p, trc, err := players.Register(ctx, "123", "client2222", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).ToNot(BeEmpty())
|
|
Expect(p.ID).ToNot(Equal("123"))
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(p.Client).To(Equal("client2222"))
|
|
Expect(trc).To(BeNil())
|
|
})
|
|
|
|
It("does not reuse another user's player by ID", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "otheruser", UserAgent: "Pixel", TranscodingId: "1"}
|
|
repo.add(plr)
|
|
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).ToNot(Equal("123"))
|
|
Expect(p.UserId).To(Equal("userid"))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
Expect(trc).To(BeNil())
|
|
})
|
|
|
|
It("finds players by ID", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", LastSeen: time.Time{}}
|
|
repo.add(plr)
|
|
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).To(Equal("123"))
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
Expect(trc).To(BeNil())
|
|
})
|
|
|
|
It("finds player by client and user names when ID is not found", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", UserAgent: "chrome", LastSeen: time.Time{}}
|
|
repo.add(plr)
|
|
p, _, err := players.Register(ctx, "999", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).To(Equal("123"))
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
})
|
|
|
|
It("finds player by client and user names when not ID is provided", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", UserAgent: "chrome", LastSeen: time.Time{}}
|
|
repo.add(plr)
|
|
p, _, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).To(Equal("123"))
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
})
|
|
|
|
It("finds player by ID and return its transcoding", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", LastSeen: time.Time{}, TranscodingId: "1"}
|
|
repo.add(plr)
|
|
p, trc, err := players.Register(ctx, "123", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).To(Equal("123"))
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
Expect(trc.ID).To(Equal("1"))
|
|
})
|
|
|
|
Context("bad username casing", func() {
|
|
ctx := log.NewContext(context.TODO())
|
|
ctx = request.WithUser(ctx, model.User{ID: "userid", UserName: "Johndoe"})
|
|
ctx = request.WithUsername(ctx, "Johndoe")
|
|
|
|
It("finds player by client and user names when not ID is provided", func() {
|
|
plr := &model.Player{ID: "123", Name: "A Player", Client: "client", UserId: "userid", UserAgent: "chrome", LastSeen: time.Time{}}
|
|
repo.add(plr)
|
|
p, _, err := players.Register(ctx, "", "client", "chrome", "1.2.3.4")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(p.ID).To(Equal("123"))
|
|
Expect(p.LastSeen).To(BeTemporally(">=", beforeRegister))
|
|
Expect(repo.lastSaved).To(Equal(p))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
type mockPlayerRepository struct {
|
|
model.PlayerRepository
|
|
lastSaved *model.Player
|
|
data map[string]model.Player
|
|
}
|
|
|
|
func (m *mockPlayerRepository) add(p *model.Player) {
|
|
if m.data == nil {
|
|
m.data = make(map[string]model.Player)
|
|
}
|
|
m.data[p.ID] = *p
|
|
}
|
|
|
|
func (m *mockPlayerRepository) Get(id string) (*model.Player, error) {
|
|
if p, ok := m.data[id]; ok {
|
|
return &p, nil
|
|
}
|
|
return nil, model.ErrNotFound
|
|
}
|
|
|
|
func (m *mockPlayerRepository) FindMatch(userId, client, userAgent string) (*model.Player, error) {
|
|
for _, p := range m.data {
|
|
if p.Client == client && p.UserId == userId && p.UserAgent == userAgent {
|
|
return &p, nil
|
|
}
|
|
}
|
|
return nil, model.ErrNotFound
|
|
}
|
|
|
|
func (m *mockPlayerRepository) Put(p *model.Player) error {
|
|
m.lastSaved = p
|
|
return nil
|
|
}
|