diff --git a/pkg/service/handlers/handlers_bmx.go b/pkg/service/handlers/handlers_bmx.go index 3d78fac..6431c25 100644 --- a/pkg/service/handlers/handlers_bmx.go +++ b/pkg/service/handlers/handlers_bmx.go @@ -182,10 +182,18 @@ func (s *Server) HandleOrionToken(w http.ResponseWriter, _ *http.Request) { // rewrap it into the Bose BmxPlaybackResponse shape via // bmx.PlayCustomStream. // -// No auth check: the data is the speaker's own input, nothing -// privileged is being protected, and soundcork's reference impl -// behaves the same way. +// Requires a Bearer token in the `Authorization` header — same as +// the rest of the BMX playback surface (TuneIn variants and the +// orion token endpoint). Real speakers obtain the token via +// POST /core02/svc-bmx-adapter-orion/prod/orion/token (HandleOrionToken) +// before they ever follow a LOCAL_INTERNET_RADIO preset, so this +// check shouldn't cost any legitimate caller. func (s *Server) HandleOrionPlayback(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") == "" { + s.writeBMXUnauthorized(w) + return + } + data := r.URL.Query().Get("data") resp, err := bmx.PlayCustomStream(data) diff --git a/pkg/service/handlers/handlers_bmx_test.go b/pkg/service/handlers/handlers_bmx_test.go index b7be322..180b3bc 100644 --- a/pkg/service/handlers/handlers_bmx_test.go +++ b/pkg/service/handlers/handlers_bmx_test.go @@ -94,6 +94,7 @@ func TestOrionPlayback(t *testing.T) { // emits (Go's url package re-encodes any `=` padding for transport). req, _ := http.NewRequest("GET", ts.URL+"/core02/svc-bmx-adapter-orion/prod/orion/station?data="+url.QueryEscape(data), nil) + req.Header.Set("Authorization", "Bearer mock-token") res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) @@ -170,11 +171,7 @@ func TestBMXUnauthorized(t *testing.T) { {"GET", "/bmx/tunein/v1/playback/station/s123"}, {"GET", "/bmx/tunein/v1/playback/episodes/p123"}, {"GET", "/bmx/tunein/v1/playback/episode/p123"}, - // Note: /core02/.../prod/orion/station is intentionally NOT in this - // list — orion playback takes its `data` blob from the speaker's - // own preset payload, there's no privileged material to gate, and - // soundcork's reference impl makes the same call (no auth on the - // station endpoint). See HandleOrionPlayback's doc comment. + {"GET", "/core02/svc-bmx-adapter-orion/prod/orion/station?data=AAAA"}, } for _, tc := range paths { diff --git a/pkg/service/handlers/issue218_regression_test.go b/pkg/service/handlers/issue218_regression_test.go index e069e84..2bfd751 100644 --- a/pkg/service/handlers/issue218_regression_test.go +++ b/pkg/service/handlers/issue218_regression_test.go @@ -79,7 +79,17 @@ func TestIssue218_OrionStationResolvesPresetStreamURL(t *testing.T) { resolved := ts.URL + parsedLocation.RequestURI() - resp, err := http.Get(resolved) //nolint:noctx + // Real speakers retrieve an orion token from + // POST /core02/svc-bmx-adapter-orion/prod/orion/token before they + // ever follow a LOCAL_INTERNET_RADIO preset; the playback handler + // rejects an empty Authorization header for parity with the other + // BMX playback routes. Use a sentinel Bearer token to match that + // shape — HandleOrionPlayback doesn't validate the token contents, + // only its presence. + req, _ := http.NewRequest("GET", resolved, nil) + req.Header.Set("Authorization", "Bearer mock-token") + + resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET %s: %v", resolved, err) }