236637fcd3
Adds SignStreamToken / VerifyStreamToken (HMAC-SHA256 over trackID|exp) and modifies handleGetStream to accept either the existing session cookie OR a valid signed token. Stream route moved out of the authed group so the handler's own auth check runs and the token bypass is reachable. Enables Sonos / UPnP speakers to fetch the stream URL without carrying the user's session cookie - they cannot. The token is short-lived (max 24h per the design); expiry checked at request time only, not per-byte, so long tracks play through. streamSecret field on handlers is nil for now; Task 2 wires the loader (env var with auto-generated fallback persisted in app_preferences). Adds auth.OptionalUser - the permissive sibling of RequireUser that attaches the user to context when a valid cookie / bearer is present but does NOT 401 on absence. The stream route is wrapped with it so the handler can fall through to the token path when no session is present. newLibraryRouter (test fixture) gets a synthetic-user middleware on the stream route so existing media_test tests keep passing without seeding a real session row - production traffic uses auth.OptionalUser, the test path uses auth.UserCtxKeyForTest(). Five tests cover round-trip, tampered token rejection, expiry, wrong-track-ID, and wrong-secret rejection. CI verifies.
79 lines
1.9 KiB
Go
79 lines
1.9 KiB
Go
package api
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSignStreamToken_RoundTrip(t *testing.T) {
|
|
secret := []byte("test-secret-not-real")
|
|
trackID := "abc-123"
|
|
exp := time.Now().Unix() + 3600
|
|
|
|
token := SignStreamToken(secret, trackID, exp)
|
|
if token == "" {
|
|
t.Fatal("got empty token")
|
|
}
|
|
if !VerifyStreamToken(secret, trackID, exp, token) {
|
|
t.Fatal("round-trip verify failed")
|
|
}
|
|
}
|
|
|
|
func TestVerifyStreamToken_TamperedTokenRejected(t *testing.T) {
|
|
secret := []byte("test-secret")
|
|
trackID := "abc-123"
|
|
exp := time.Now().Unix() + 3600
|
|
|
|
token := SignStreamToken(secret, trackID, exp)
|
|
// Flip a hex digit anywhere in the middle.
|
|
mid := len(token) / 2
|
|
tampered := token[:mid] + flipHexDigit(token[mid:mid+1]) + token[mid+1:]
|
|
|
|
if VerifyStreamToken(secret, trackID, exp, tampered) {
|
|
t.Fatal("verify accepted tampered token")
|
|
}
|
|
}
|
|
|
|
func TestVerifyStreamToken_ExpiredRejected(t *testing.T) {
|
|
secret := []byte("test-secret")
|
|
trackID := "abc-123"
|
|
exp := time.Now().Unix() - 1 // already expired
|
|
|
|
token := SignStreamToken(secret, trackID, exp)
|
|
if VerifyStreamToken(secret, trackID, exp, token) {
|
|
t.Fatal("verify accepted expired token")
|
|
}
|
|
}
|
|
|
|
func TestVerifyStreamToken_WrongTrackIDRejected(t *testing.T) {
|
|
secret := []byte("test-secret")
|
|
exp := time.Now().Unix() + 3600
|
|
|
|
token := SignStreamToken(secret, "track-a", exp)
|
|
if VerifyStreamToken(secret, "track-b", exp, token) {
|
|
t.Fatal("verify accepted token for different track")
|
|
}
|
|
}
|
|
|
|
func TestVerifyStreamToken_WrongSecretRejected(t *testing.T) {
|
|
trackID := "abc-123"
|
|
exp := time.Now().Unix() + 3600
|
|
|
|
token := SignStreamToken([]byte("secret-a"), trackID, exp)
|
|
if VerifyStreamToken([]byte("secret-b"), trackID, exp, token) {
|
|
t.Fatal("verify accepted token signed with different secret")
|
|
}
|
|
}
|
|
|
|
func flipHexDigit(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
switch s[0] {
|
|
case '0':
|
|
return "1"
|
|
default:
|
|
return "0"
|
|
}
|
|
}
|