Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/v1_playlist_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ func (app *ApiServer) v1PlaylistStream(c *fiber.Ctx) error {
continue
}

// Leave deleted tracks, and tracks whose owner is no longer active, out
// of the m3u8 entirely rather than emitting a URL the stream endpoint
// will now reject.
if !track.IsStreamable {
continue
}

// Get track duration
duration := int32(0)
if track.Duration.Valid {
Expand Down
7 changes: 7 additions & 0 deletions api/v1_track_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ func (app *ApiServer) v1TrackDownload(c *fiber.Ctx) error {
}

track := tracks[0]

// Same guard as the stream endpoint: a deleted track, or one whose owner is
// no longer active, must not have its audio served here either.
if !track.IsStreamable {
return fiber.NewError(fiber.StatusNotFound, "track not found")
}

if !track.Access.Download {
return fiber.NewError(fiber.StatusForbidden, "you are not allowed to download this track")
}
Expand Down
10 changes: 10 additions & 0 deletions api/v1_track_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ func (app *ApiServer) v1TrackStream(c *fiber.Ctx) error {

track := tracks[0]

// `is_streamable` is false when the track is deleted or its owner is no
// longer active - either the artist deactivated their own account or the
// account was delisted by the trusted notifier. The track response has
// always reported this, but nothing enforced it, so the audio stayed
// reachable to anyone holding the URL. Treat it as not found rather than
// forbidden so we don't distinguish these from a missing track.
if !track.IsStreamable {
return fiber.NewError(fiber.StatusNotFound, "track not found")
}

if track.Access.Stream {
// Stream is nil when the track row has no cid to sign (e.g. an
// upload-v2 row that never got its track_cid backfilled).
Expand Down
59 changes: 59 additions & 0 deletions api/v1_track_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,62 @@ func TestGetTrackStreamWithID3(t *testing.T) {
assert.Contains(t, location, "id3=true")
assert.Contains(t, location, "id3_title=Culca+Canyon")
}

// A track whose owner is no longer active - the artist deactivated their own
// account, or the account was delisted by the trusted notifier - reports
// is_streamable=false on the track response. The stream endpoint must refuse
// to serve the audio rather than redirecting to a signed content-node URL.
func TestGetTrackStream_DeactivatedOwner(t *testing.T) {
app := emptyTestApp(t)
fixtures := database.FixtureMap{
"tracks": []map[string]any{
{
"track_id": 1,
"owner_id": 1,
"title": "Deactivated Owner",
"track_cid": "QmDeactivatedOwnerCid",
},
},
"users": []map[string]any{
{
"user_id": 1,
"handle": "testuser1",
"is_deactivated": true,
},
},
}
database.Seed(app.pool.Replicas[0], fixtures)
req := httptest.NewRequest("GET", "/v1/tracks/"+trashid.MustEncodeHashID(1)+"/stream", nil)
res, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, 404, res.StatusCode)
assert.Empty(t, res.Header.Get("Location"))
}

// A deleted track is likewise non-streamable and must not redirect.
func TestGetTrackStream_DeletedTrack(t *testing.T) {
app := emptyTestApp(t)
fixtures := database.FixtureMap{
"tracks": []map[string]any{
{
"track_id": 1,
"owner_id": 1,
"title": "Deleted",
"track_cid": "QmDeletedTrackCid",
"is_delete": true,
},
},
"users": []map[string]any{
{
"user_id": 1,
"handle": "testuser1",
},
},
}
database.Seed(app.pool.Replicas[0], fixtures)
req := httptest.NewRequest("GET", "/v1/tracks/"+trashid.MustEncodeHashID(1)+"/stream", nil)
res, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, 404, res.StatusCode)
assert.Empty(t, res.Header.Get("Location"))
}
Loading