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: 6 additions & 1 deletion internal/api/handlers/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ func NewStreamHandler(sessionMgr SessionManagerInterface, fileResolver FilePathR
// For transcode: returns 400 (transcode uses manifest/segment endpoints).
func (h *StreamHandler) HandleStream(w http.ResponseWriter, r *http.Request) {
userID := apimw.GetUserID(r.Context())
if userID == 0 {
// A verified, session-bound stream token (StreamTokenAuth) authorizes
// delivery of this session's bytes without a user, exactly as it does for
// the HLS manifest and segments: native TV players fetch the plan URL and
// cannot send a bearer. The session load below still enforces the session's
// own media-authorization mode, and a present identity must still own it.
if userID == 0 && !apimw.IsStreamTokenAuthorized(r.Context()) {
writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required")
return
}
Expand Down
70 changes: 70 additions & 0 deletions internal/api/handlers/stream_token_delivery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package handlers

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/go-chi/chi/v5"

apimw "github.com/prairie-server/prairie-server/internal/api/middleware"
"github.com/prairie-server/prairie-server/internal/models"
"github.com/prairie-server/prairie-server/internal/playback"
"github.com/prairie-server/prairie-server/internal/streamtoken"
)

// A native TV player fetches the plan's /api/v2/stream/{id}?st=… URL with no
// bearer. The session-bound stream token must authorize those bytes exactly as
// it does the HLS manifest and segments; before this, every direct-play and
// progressive-remux start on Tizen 401'd and fell back to an encode.
func TestHandleStream_StreamTokenAuthorizesAnonymousDelivery(t *testing.T) {
const secret = "test-stream-token-secret"
filePath := writePlaybackTestMediaFile(t, "movie.mp4")
file := &models.MediaFile{ID: 42, ContentID: "movie-1", FilePath: filePath, Duration: 3600}
sessionMgr := playback.NewSessionManager(0, 0)
session, err := sessionMgr.StartSession(1, "profile-1", 42, playback.PlayDirect, false)
if err != nil {
t.Fatalf("StartSession: %v", err)
}
handler := NewStreamHandler(sessionMgr, testPlaybackFileResolver{file: file})
handler.JWTSecret = secret
gated := (&apimw.AuthMiddleware{}).StreamTokenAuth(secret)(http.HandlerFunc(handler.HandleStream))

serve := func(token string) *httptest.ResponseRecorder {
t.Helper()
target := "/api/v2/stream/" + session.ID
if token != "" {
target += "?st=" + url.QueryEscape(token)
}
req := httptest.NewRequest(http.MethodGet, target, nil)
routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("session_id", session.ID)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, routeCtx))
rr := httptest.NewRecorder()
gated.ServeHTTP(rr, req)
return rr
}

if rr := serve(""); rr.Code != http.StatusUnauthorized {
t.Fatalf("no credential: status = %d, want 401", rr.Code)
}

other, err := streamtoken.Sign(streamtoken.Claims{SessionID: "another-session"}, secret, time.Hour)
if err != nil {
t.Fatalf("Sign: %v", err)
}
if rr := serve(other); rr.Code != http.StatusUnauthorized {
t.Fatalf("token for another session: status = %d, want 401", rr.Code)
}

token, err := streamtoken.Sign(streamtoken.Claims{SessionID: session.ID}, secret, time.Hour)
if err != nil {
t.Fatalf("Sign: %v", err)
}
if rr := serve(token); rr.Code != http.StatusOK {
t.Fatalf("session-bound token: status = %d, body = %s", rr.Code, rr.Body.String())
}
}
1 change: 1 addition & 0 deletions scripts/prairie-invariants.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ web/src/api/client.ts 1 X-Prairie- client identity headers
# --- Stats for nerds (event log + planner detail).
web/src/player/components/PlaybackInfoOverlay.tsx 1 Recent events web stats overlay shows the player event log
web/src/player/components/VideoPlayer.tsx 1 new PlaybackEventLog\( web player records events for the stats overlay
internal/api/handlers/stream.go 1 userID == 0 && !apimw\.IsStreamTokenAuthorized\( v2 progressive/direct delivery honors st (Tizen direct play)
Loading