fix(bmx): relax TuneIn + Orion Authorization gate, log instead

Seven BMX adapter handlers required a non-empty `Authorization` header
and returned 401 from writeBMXUnauthorized when missing:

  TuneIn:  Playback, PodcastInfo, PlaybackPodcast, Report, Navigate, Search
  Orion:   Playback

Speakers calling these endpoints directly carry their margeAuthToken in
the header, so the gate works for them. But the Stockholm browser
proxy (pkg/service/stockholm/proxy.go injectBackendHeaders) only injects
Authorization for hosts ending in .bose.com or .apigee.net with a marge
path — when Stockholm calls back into our own service for TuneIn
browsing/playback/search/etc., no header is added and every request
401s.

Disable the gate at all seven sites; log the missing-header case so the
absence remains visible. Keep writeBMXUnauthorized as the future-restore
point (//nolint:unused) — when the gate comes back (e.g. behind a
BMX_STRICT_AUTH env-var or once the Stockholm proxy learns to inject
Authorization for our own host), callers will use this helper again.

Tests that assert 401 for missing Authorization (TestBMXUnauthorized,
TestHandleTuneInReport/Unauthorized, TestHandleTuneInNavigate/Unauthorized,
TestHandleTuneInSearch/Unauthorized) are `t.Skip`'d with a pointer back
to handlers_bmx_tunein.go — they stay in the file to come back to life
the day the gate does.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-17 15:05:39 +02:00
co-authored by Claude Opus 4.7
parent d7bbc09ce6
commit b9c1cdad29
6 changed files with 55 additions and 16 deletions
+5 -2
View File
@@ -45,8 +45,11 @@ func (s *Server) HandleBMXServicesAvailability(w http.ResponseWriter, _ *http.Re
// writeBMXUnauthorized writes the canonical 401 used by every BMX adapter
// handler that requires an Authorization header (TuneIn variants, Orion
// playback). Kept here so all per-service files can share it without
// duplicating the body markup.
// playback). Currently unused because all gate sites are temporarily
// disabled (log-only); kept as the future-restore point — when we re-add
// the gate, callers will use this helper.
//
//nolint:unused // intentional: future-restore point for the disabled auth gate.
func (s *Server) writeBMXUnauthorized(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
+6 -2
View File
@@ -7,6 +7,7 @@ package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/gesellix/bose-soundtouch/pkg/service/bmx"
@@ -52,9 +53,12 @@ func (s *Server) HandleOrionToken(w http.ResponseWriter, _ *http.Request) {
// 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) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// See HandleTuneInPlayback for the rationale. Logged so we can spot
// callers that would have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
data := r.URL.Query().Get("data")
@@ -73,6 +73,8 @@ func TestHandleTuneInReport(t *testing.T) {
})
t.Run("Unauthorized", func(t *testing.T) {
t.Skip("auth gate temporarily disabled in handlers_bmx_tunein.go; restore this assertion when the gate is re-enabled")
req, _ := http.NewRequest("POST", ts.URL+"/bmx/tunein/v1/report", nil)
res, err := http.DefaultClient.Do(req)
if err != nil {
@@ -159,6 +159,8 @@ func TestCustomPlayback(t *testing.T) {
}
func TestBMXUnauthorized(t *testing.T) {
t.Skip("auth gate temporarily disabled in handlers_bmx_tunein.go and handlers_bmx_orion.go; restore this assertion when the gate is re-enabled")
r, _ := setupRouter("http://localhost:8001", nil)
ts := httptest.NewServer(r)
+36 -12
View File
@@ -38,9 +38,13 @@ func (s *Server) tuneInStreamFormats() string {
// HandleTuneInPlayback returns TuneIn playback information.
func (s *Server) HandleTuneInPlayback(w http.ResponseWriter, r *http.Request) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// The Stockholm browser proxy doesn't inject Authorization for requests
// that target our own service. Logged so we can spot callers that would
// have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
stationID := chi.URLParam(r, "stationID")
@@ -61,9 +65,13 @@ func (s *Server) HandleTuneInPlayback(w http.ResponseWriter, r *http.Request) {
// HandleTuneInPodcastInfo returns TuneIn podcast information.
func (s *Server) HandleTuneInPodcastInfo(w http.ResponseWriter, r *http.Request) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// The Stockholm browser proxy doesn't inject Authorization for requests
// that target our own service. Logged so we can spot callers that would
// have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
podcastID := chi.URLParam(r, "podcastID")
@@ -85,9 +93,13 @@ func (s *Server) HandleTuneInPodcastInfo(w http.ResponseWriter, r *http.Request)
// HandleTuneInPlaybackPodcast returns TuneIn podcast playback information.
func (s *Server) HandleTuneInPlaybackPodcast(w http.ResponseWriter, r *http.Request) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// The Stockholm browser proxy doesn't inject Authorization for requests
// that target our own service. Logged so we can spot callers that would
// have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
podcastID := chi.URLParam(r, "podcastID")
@@ -135,9 +147,13 @@ func (s *Server) HandleTuneInToken(w http.ResponseWriter, r *http.Request) {
// HandleTuneInReport handles TuneIn playback reporting.
func (s *Server) HandleTuneInReport(w http.ResponseWriter, r *http.Request) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// The Stockholm browser proxy doesn't inject Authorization for requests
// that target our own service. Logged so we can spot callers that would
// have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
var req struct {
@@ -179,9 +195,13 @@ func (s *Server) HandleTuneInReport(w http.ResponseWriter, r *http.Request) {
// - sub/{n}/{encodedURI} → single subsection of a browse page
// - profiles/{type}/{id}/{encodedURI} → artist/program profile page
func (s *Server) HandleTuneInNavigate(w http.ResponseWriter, r *http.Request) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// The Stockholm browser proxy doesn't inject Authorization for requests
// that target our own service. Logged so we can spot callers that would
// have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
wildcard := chi.URLParam(r, "*")
@@ -242,9 +262,13 @@ func parseTuneInNavigatePath(wildcard string) (interface{}, error) {
// HandleTuneInSearch returns live TuneIn search results for the given query.
func (s *Server) HandleTuneInSearch(w http.ResponseWriter, r *http.Request) {
// Authorization gate temporarily disabled (was: 401 if header missing).
// The Stockholm browser proxy doesn't inject Authorization for requests
// that target our own service. Logged so we can spot callers that would
// have been rejected; do NOT 401.
if r.Header.Get("Authorization") == "" {
s.writeBMXUnauthorized(w)
return
log.Printf("[BMX] Authorization missing (gate temporarily disabled, see handlers_bmx.go); path=%q ua=%q",
r.URL.Path, r.UserAgent())
}
query := r.URL.Query().Get("q")
@@ -47,6 +47,8 @@ func TestHandleTuneInNavigate(t *testing.T) {
})
t.Run("Unauthorized", func(t *testing.T) {
t.Skip("auth gate temporarily disabled in handlers_bmx_tunein.go; restore this assertion when the gate is re-enabled")
req := httptest.NewRequest("GET", "/bmx/tunein/v1/navigate", nil)
w := httptest.NewRecorder()
@@ -83,6 +85,8 @@ func TestHandleTuneInSearch(t *testing.T) {
})
t.Run("Unauthorized", func(t *testing.T) {
t.Skip("auth gate temporarily disabled in handlers_bmx_tunein.go; restore this assertion when the gate is re-enabled")
req := httptest.NewRequest("GET", "/bmx/tunein/v1/search?q=music", nil)
w := httptest.NewRecorder()