mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
fix(bmx): strip trailing slash from server_url so TuneIn playback routes
A server_url configured with a trailing slash (e.g. http://host:8000/) flowed verbatim into the BMX registry base ("{BMX_SERVER}/bmx/tunein"), so speakers were handed "http://host:8000//bmx/tunein" and requested "//bmx/tunein/v1/playback/station/{id}". The chi router does not match the doubled-slash path, so TuneIn playback returned 404 and the speaker reported INVALID_SOURCE. Confirmed from a reporter's diagnostic export. - Add NormalizeServerURL (trim whitespace + trailing slashes); apply in NewServer so the BMX base is always clean. - Normalize server_url at ingestion in main (flag + persisted) so the margeServerUrl/bmxRegistryUrl pushed to speakers stays clean too. - Normalize in the live settings-update path so a UI-saved trailing slash is trimmed before validate/persist. - Mount chi middleware.CleanPath as a defensive net: any "//" path collapses to "/" before routing, regardless of source. - Regression tests: NormalizeServerURL table + BMX registry must not emit "//bmx"/"//media" for a trailing-slash server_url. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
519526852d
commit
4f561944a3
@@ -693,6 +693,9 @@ func loadConfig(c *cli.Context) serviceConfig {
|
||||
if serverURL == "" {
|
||||
serverURL = "http://" + hostname + ":" + port
|
||||
}
|
||||
// Strip a trailing slash so it cannot leak into the BMX registry base or the
|
||||
// margeServerUrl/bmxRegistryUrl pushed to speakers during migration.
|
||||
serverURL = handlers.NormalizeServerURL(serverURL)
|
||||
|
||||
httpsPort := c.String("https-port")
|
||||
|
||||
@@ -878,7 +881,7 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
|
||||
}
|
||||
|
||||
if persisted.ServerURL != "" {
|
||||
config.serverURL = persisted.ServerURL
|
||||
config.serverURL = handlers.NormalizeServerURL(persisted.ServerURL)
|
||||
}
|
||||
|
||||
if persisted.HTTPServerURL != "" {
|
||||
@@ -1060,6 +1063,13 @@ func startDeviceDiscovery(server *handlers.Server) {
|
||||
func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// CleanPath collapses duplicate slashes ("//bmx/..." -> "/bmx/...") and
|
||||
// resolves . / .. before routing. Defensive net for the double-slash
|
||||
// playback bug: even if a misconfigured base URL hands a speaker a "//bmx"
|
||||
// path, it still reaches the right handler instead of 404ing. Runs first so
|
||||
// every downstream middleware and the recorder see the cleaned path.
|
||||
r.Use(middleware.CleanPath)
|
||||
|
||||
// TrustedRealIP must run before any handler that reads r.RemoteAddr —
|
||||
// SnapshotMiddleware captures the request, and several handlers
|
||||
// (HandleMargePowerOn, etc.) inspect the source IP. The middleware is
|
||||
|
||||
@@ -105,3 +105,70 @@ func TestHandleBMXRegistry_DNSDependent(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeServerURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"http://host:8000", "http://host:8000"},
|
||||
{"http://host:8000/", "http://host:8000"},
|
||||
{"http://host:8000///", "http://host:8000"},
|
||||
{" http://host:8000/ ", "http://host:8000"},
|
||||
{"https://127.0.0.1", "https://127.0.0.1"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := NormalizeServerURL(c.in); got != c.want {
|
||||
t.Errorf("NormalizeServerURL(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleBMXRegistry_TrailingSlashServerURL is a regression test for the
|
||||
// trailing-slash double-slash bug: a server_url configured with a trailing
|
||||
// slash must not produce a "//bmx/..."
|
||||
// base URL. The speaker concatenates "/v1/playback/station/{id}" onto the base,
|
||||
// so a doubled slash yields a "//bmx/tunein/..." request the router 404s and
|
||||
// TuneIn playback fails with INVALID_SOURCE.
|
||||
func TestHandleBMXRegistry_TrailingSlashServerURL(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "bmx-registry-slash-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
_ = ds.Initialize()
|
||||
|
||||
// Operator typed a trailing slash (the reported trailing-slash case).
|
||||
server := NewServer(ds, nil, "https://127.0.0.1/", false, false, false)
|
||||
server.SetDNSSettings(false, "", "")
|
||||
|
||||
req := httptest.NewRequest("GET", "/bmx/v1/services", nil)
|
||||
w := httptest.NewRecorder()
|
||||
server.HandleBMXRegistry(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "//bmx") || strings.Contains(body, "//media") {
|
||||
t.Errorf("registry response contains a doubled slash from the trailing-slash server_url:\n%s", body)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(body), &resp); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
for _, s := range resp["bmx_services"].([]interface{}) {
|
||||
service := s.(map[string]interface{})
|
||||
if service["id"].(map[string]interface{})["name"] == "TUNEIN" {
|
||||
if baseURL := service["baseUrl"].(string); baseURL != "https://127.0.0.1/bmx/tunein" {
|
||||
t.Errorf("Expected baseUrl https://127.0.0.1/bmx/tunein, got %s", baseURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("TuneIn service not found in registry")
|
||||
}
|
||||
|
||||
@@ -294,6 +294,11 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[DNS] DNS Discovery enabled without explicit upstreams, will try system DNS.")
|
||||
}
|
||||
|
||||
// Strip a trailing slash before validating/persisting: it would otherwise
|
||||
// flow into the BMX registry base and produce "//bmx/..." playback requests
|
||||
// the router 404s.
|
||||
settings.ServerURL = NormalizeServerURL(settings.ServerURL)
|
||||
|
||||
// Validate server_url: the same value the DNS server uses to derive its
|
||||
// intercept IP. Reject anything that does not resolve to a routable IP so
|
||||
// users see the error in the UI instead of getting a silently-broken setup
|
||||
|
||||
@@ -108,12 +108,23 @@ var bufferPool = sync.Pool{
|
||||
},
|
||||
}
|
||||
|
||||
// NormalizeServerURL trims surrounding whitespace and any trailing slashes from
|
||||
// a configured server URL. A trailing slash poisons every URL built by string
|
||||
// concatenation from it, most visibly the BMX registry base ("{BMX_SERVER}/bmx/
|
||||
// tunein" in bmx_services.json): it would otherwise hand a speaker
|
||||
// "http://host:8000//bmx/tunein" and make it request "//bmx/tunein/...", a path
|
||||
// the chi router does not match, so playback 404s. It also keeps {MEDIA_SERVER}
|
||||
// and the OAuth redirect URIs free of a stray double slash.
|
||||
func NormalizeServerURL(serverURL string) string {
|
||||
return strings.TrimRight(strings.TrimSpace(serverURL), "/")
|
||||
}
|
||||
|
||||
// NewServer creates a new SoundTouch service server.
|
||||
func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, redactLogs, logBodies, recordEnabled bool) *Server {
|
||||
s := &Server{
|
||||
ds: ds,
|
||||
sm: sm,
|
||||
serverURL: serverURL,
|
||||
serverURL: NormalizeServerURL(serverURL),
|
||||
redactLogs: redactLogs,
|
||||
logBodies: logBodies,
|
||||
recordEnabled: recordEnabled,
|
||||
|
||||
Reference in New Issue
Block a user