mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 00:26:29 +00:00
fix(tts): move TTS endpoints from /mgmt to /setup (no Basic Auth)
The TTS speak/config endpoints were under /mgmt (Basic-Auth protected),
but the soundtouch-web proxy and CLI authenticated with their own
mgmt-password default (empty) while the service defaults to "change_me!",
so speaking from -web returned 401.
This was also inconsistent: the Google API key is configured via the
unauthenticated /setup/settings, and Play URL already proxies to /setup,
so gating only TTS playback behind mgmt auth made no sense. Move
/mgmt/tts/{speak,config} to /setup/tts/{speak,config} (LAN-trust, like
the rest of the setup surface), rename the handlers accordingly, and drop
the now-unused mgmt-credential plumbing from soundtouch-web and the CLI
tts command.
Verified: POST /setup/tts/speak now reaches the handler without auth
(502 only because the test speaker IP is unreachable; previously 401).
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
8f2939a9a6
commit
169c1c5b9f
@@ -65,17 +65,6 @@ func ttsSpeakCmd() *cli.Command {
|
||||
Aliases: []string{"v"},
|
||||
Usage: "Playback volume (0-100, 0 = service default)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mgmt-username",
|
||||
Usage: "Management API username for HTTP Basic Auth",
|
||||
Value: "admin",
|
||||
EnvVars: []string{"MGMT_USERNAME"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mgmt-password",
|
||||
Usage: "Management API password for HTTP Basic Auth",
|
||||
EnvVars: []string{"MGMT_PASSWORD"},
|
||||
},
|
||||
),
|
||||
Action: ttsSpeak,
|
||||
}
|
||||
@@ -116,13 +105,12 @@ func ttsSpeak(c *cli.Context) error {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, serviceURL+"/mgmt/tts/speak", bytes.NewReader(body))
|
||||
req, err := http.NewRequest(http.MethodPost, serviceURL+"/setup/tts/speak", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.SetBasicAuth(c.String("mgmt-username"), c.String("mgmt-password"))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -1321,11 +1321,6 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) *
|
||||
r.Post("/prime", server.HandleMgmtPrimeDeviceAmazon)
|
||||
})
|
||||
|
||||
r.Route("/tts", func(r chi.Router) {
|
||||
r.Post("/speak", server.HandleMgmtTTSSpeak)
|
||||
r.Get("/config", server.HandleMgmtTTSConfig)
|
||||
})
|
||||
|
||||
r.Get("/devices/{deviceId}/events", server.HandleMgmtDeviceEvents)
|
||||
})
|
||||
})
|
||||
@@ -1338,6 +1333,11 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) *
|
||||
r.Get("/discovery-status", server.HandleGetDiscoveryStatus)
|
||||
r.Get("/settings", server.HandleGetSettings)
|
||||
r.Post("/settings", server.HandleUpdateSettings)
|
||||
// TTS lives under /setup (LAN-trust, like the rest of the integration
|
||||
// surface and Play URL), not /mgmt: the API key is already configured
|
||||
// via /setup/settings, and -web/CLI reach this without mgmt credentials.
|
||||
r.Post("/tts/speak", server.HandleTTSSpeak)
|
||||
r.Get("/tts/config", server.HandleTTSConfig)
|
||||
r.Get("/info/{deviceId}", server.HandleGetDeviceInfo)
|
||||
r.Get("/summary/{deviceId}", server.HandleGetMigrationSummary)
|
||||
r.Post("/migrate/{deviceId}", server.HandleMigrateDevice)
|
||||
|
||||
+2
-2
@@ -59,7 +59,6 @@ GET /mgmt/devices/{deviceId}/events handlers.(
|
||||
GET /mgmt/spotify/accounts handlers.(*Server).HandleMgmtSpotifyAccounts-fm
|
||||
GET /mgmt/spotify/callback handlers.(*Server).HandleMgmtSpotifyCallback-fm
|
||||
GET /mgmt/spotify/token handlers.(*Server).HandleMgmtSpotifyToken-fm
|
||||
GET /mgmt/tts/config handlers.(*Server).HandleMgmtTTSConfig-fm
|
||||
GET /setup/account-id-suggestions/{deviceId} handlers.(*Server).HandleAccountIDSuggestions-fm
|
||||
GET /setup/ca.crt handlers.(*Server).HandleGetCACert-fm
|
||||
GET /setup/device-summary/{deviceId} handlers.(*Server).HandleDeviceSummary-fm
|
||||
@@ -79,6 +78,7 @@ GET /setup/logging-settings handlers.(
|
||||
GET /setup/logs handlers.(*Server).HandleGetLogs-fm
|
||||
GET /setup/settings handlers.(*Server).HandleGetSettings-fm
|
||||
GET /setup/summary/{deviceId} handlers.(*Server).HandleGetMigrationSummary-fm
|
||||
GET /setup/tts/config handlers.(*Server).HandleTTSConfig-fm
|
||||
GET /setup/version handlers.(*Server).HandleGetVersionInfo-fm
|
||||
GET /streaming/account/{account}/device/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm
|
||||
GET /streaming/account/{account}/device/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm
|
||||
@@ -132,7 +132,6 @@ POST /mgmt/spotify/confirm handlers.(
|
||||
POST /mgmt/spotify/entity handlers.(*Server).HandleMgmtSpotifyEntity-fm
|
||||
POST /mgmt/spotify/init handlers.(*Server).HandleMgmtSpotifyInit-fm
|
||||
POST /mgmt/spotify/prime handlers.(*Server).HandleMgmtPrimeDevice-fm
|
||||
POST /mgmt/tts/speak handlers.(*Server).HandleMgmtTTSSpeak-fm
|
||||
POST /oauth/account/{account}/music/musicprovider/{sourceID}/token/cs handlers.(*Server).HandleBoseAccountToken-fm
|
||||
POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token handlers.(*Server).HandleBoseLegacyToken-fm
|
||||
POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs1 handlers.(*Server).HandleBoseToken-fm
|
||||
@@ -155,6 +154,7 @@ POST /setup/test-connection/{deviceId} handlers.(
|
||||
POST /setup/test-dns/{deviceId} handlers.(*Server).HandleTestDNSRedirection-fm
|
||||
POST /setup/test-hosts/{deviceId} handlers.(*Server).HandleTestHostsRedirection-fm
|
||||
POST /setup/trust-ca/{deviceId} handlers.(*Server).HandleTrustCACert-fm
|
||||
POST /setup/tts/speak handlers.(*Server).HandleTTSSpeak-fm
|
||||
POST /streaming/account handlers.(*Server).HandleMargeCreateAccount-fm
|
||||
POST /streaming/account/login handlers.(*Server).HandleMargeLogin-fm
|
||||
POST /streaming/account/{account}/device/ handlers.(*Server).HandleMargeAddDevice-fm
|
||||
|
||||
@@ -81,17 +81,6 @@ func main() {
|
||||
Usage: "AfterTouch service base URL (e.g. https://soundtouch.local). Required for custom stream URLs to work as presets via LOCAL_INTERNET_RADIO",
|
||||
EnvVars: []string{"SERVICE_URL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mgmt-username",
|
||||
Usage: "AfterTouch management API username, used to proxy TTS to the service's /mgmt endpoints",
|
||||
Value: "admin",
|
||||
EnvVars: []string{"MGMT_USERNAME"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mgmt-password",
|
||||
Usage: "AfterTouch management API password, used to proxy TTS to the service's /mgmt endpoints",
|
||||
EnvVars: []string{"MGMT_PASSWORD"},
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
port := c.String("port")
|
||||
@@ -126,8 +115,6 @@ func main() {
|
||||
webApp.Date = date
|
||||
webApp.RepoURL = repoURL
|
||||
webApp.ServiceURL = strings.TrimRight(c.String("service-url"), "/")
|
||||
webApp.MgmtUsername = c.String("mgmt-username")
|
||||
webApp.MgmtPassword = c.String("mgmt-password")
|
||||
|
||||
discoveryService := soundtouchweb.NewDiscoveryService(ifaceName)
|
||||
|
||||
|
||||
@@ -308,10 +308,11 @@ soundtouch-service
|
||||
|
||||
### Triggering speech
|
||||
|
||||
Service management API (Basic Auth):
|
||||
Service HTTP API (under `/setup`, LAN-trust like the rest of the setup surface,
|
||||
no auth):
|
||||
|
||||
```bash
|
||||
curl -u admin:change_me! -X POST http://soundtouch.local:8000/mgmt/tts/speak \
|
||||
curl -X POST http://soundtouch.local:8000/setup/tts/speak \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"host":"192.0.2.100","text":"Dinner is ready"}'
|
||||
```
|
||||
@@ -328,9 +329,9 @@ soundtouch-cli tts speak \
|
||||
--text "Dinner is ready"
|
||||
```
|
||||
|
||||
Web UI: the per-device controls include a "Say something…" box. soundtouch-web
|
||||
proxies it to the service, so start it with `--service-url` (and `--mgmt-username`
|
||||
/ `--mgmt-password` if you changed the defaults).
|
||||
Web UI: the TTS source view (and the Play URL view) include a "Say something…"
|
||||
box. soundtouch-web proxies it to the service, so start it with `--service-url`
|
||||
(or enter the service URL in the view).
|
||||
|
||||
### Notes and limitations
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ type ttsSpeakRequest struct {
|
||||
Volume *int `json:"volume,omitempty"`
|
||||
}
|
||||
|
||||
// HandleMgmtTTSSpeak synthesizes the requested text (or builds a direct URL),
|
||||
// HandleTTSSpeak synthesizes the requested text (or builds a direct URL),
|
||||
// then tells the target speaker to play it via the /speaker endpoint.
|
||||
func (s *Server) HandleMgmtTTSSpeak(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) HandleTTSSpeak(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
svc := s.ttsSvc()
|
||||
@@ -147,8 +147,8 @@ func (s *Server) HandleTTSMedia(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(audio)
|
||||
}
|
||||
|
||||
// HandleMgmtTTSConfig reports the active TTS configuration (no secrets).
|
||||
func (s *Server) HandleMgmtTTSConfig(w http.ResponseWriter, _ *http.Request) {
|
||||
// HandleTTSConfig reports the active TTS configuration (no secrets).
|
||||
func (s *Server) HandleTTSConfig(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
svc := s.ttsSvc()
|
||||
|
||||
@@ -23,8 +23,8 @@ func ttsTestRouter(t *testing.T, baseURL string) (*chi.Mux, *Server) {
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/media/tts/{id}", server.HandleTTSMedia)
|
||||
r.Post("/mgmt/tts/speak", server.HandleMgmtTTSSpeak)
|
||||
r.Get("/mgmt/tts/config", server.HandleMgmtTTSConfig)
|
||||
r.Post("/setup/tts/speak", server.HandleTTSSpeak)
|
||||
r.Get("/setup/tts/config", server.HandleTTSConfig)
|
||||
|
||||
return r, server
|
||||
}
|
||||
@@ -40,10 +40,10 @@ func mockCloudTTS(t *testing.T, audio string) *httptest.Server {
|
||||
}))
|
||||
}
|
||||
|
||||
func TestHandleMgmtTTSConfigNotConfigured(t *testing.T) {
|
||||
func TestHandleTTSConfigNotConfigured(t *testing.T) {
|
||||
r, _ := ttsTestRouter(t, "http://localhost:8001")
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/mgmt/tts/config", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/setup/tts/config", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -61,11 +61,11 @@ func TestHandleMgmtTTSConfigNotConfigured(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMgmtTTSConfigConfigured(t *testing.T) {
|
||||
func TestHandleTTSConfigConfigured(t *testing.T) {
|
||||
r, server := ttsTestRouter(t, "http://localhost:8001")
|
||||
server.SetTTSService(tts.NewService(tts.NewTranslateProvider(), tts.Config{AppKey: "k", DefaultLanguage: "EN"}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/mgmt/tts/config", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/setup/tts/config", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -139,10 +139,10 @@ func TestHandleTTSMediaMissingClip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMgmtTTSSpeakNotConfigured(t *testing.T) {
|
||||
func TestHandleTTSSpeakNotConfigured(t *testing.T) {
|
||||
r, _ := ttsTestRouter(t, "http://localhost:8001")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/mgmt/tts/speak", strings.NewReader(`{"host":"192.0.2.10","text":"hi"}`))
|
||||
req := httptest.NewRequest(http.MethodPost, "/setup/tts/speak", strings.NewReader(`{"host":"192.0.2.10","text":"hi"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestHandleMgmtTTSSpeakNotConfigured(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMgmtTTSSpeakValidation(t *testing.T) {
|
||||
func TestHandleTTSSpeakValidation(t *testing.T) {
|
||||
r, server := ttsTestRouter(t, "http://localhost:8001")
|
||||
server.SetTTSService(tts.NewService(tts.NewTranslateProvider(), tts.Config{AppKey: "k"}))
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestHandleMgmtTTSSpeakValidation(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/mgmt/tts/speak", strings.NewReader(tc.body))
|
||||
req := httptest.NewRequest(http.MethodPost, "/setup/tts/speak", strings.NewReader(tc.body))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
|
||||
@@ -42,11 +42,6 @@ type WebApp struct {
|
||||
RepoURL string
|
||||
ServiceURL string
|
||||
|
||||
// Management API credentials for proxying to the AfterTouch service's
|
||||
// Basic-Auth-protected /mgmt endpoints (e.g. TTS synthesis).
|
||||
MgmtUsername string
|
||||
MgmtPassword string
|
||||
|
||||
discoveryStatus atomic.Value // stores *webtypes.DiscoveryStatus
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// HandleAPISpeakText synthesizes and plays text on a device. The Web UI talks
|
||||
// to speakers directly for most controls, but TTS synthesis (Google Cloud) and
|
||||
// the Bose app_key live in the AfterTouch service, so this proxies to the
|
||||
// service's /mgmt/tts/speak endpoint, targeting the device by its IP/host.
|
||||
// service's /setup/tts/speak endpoint, targeting the device by its IP/host.
|
||||
func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) {
|
||||
deviceID := chi.URLParam(r, "id")
|
||||
|
||||
@@ -83,7 +83,7 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
upstream, err := http.NewRequestWithContext(r.Context(), http.MethodPost, serviceURL+"/mgmt/tts/speak", bytes.NewReader(body))
|
||||
upstream, err := http.NewRequestWithContext(r.Context(), http.MethodPost, serviceURL+"/setup/tts/speak", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
app.sendError(w, "Failed to build TTS request", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -91,10 +91,6 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
upstream.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if app.MgmtUsername != "" || app.MgmtPassword != "" {
|
||||
upstream.SetBasicAuth(app.MgmtUsername, app.MgmtPassword)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(upstream)
|
||||
if err != nil {
|
||||
app.sendError(w, fmt.Sprintf("TTS service request failed: %v", err), http.StatusBadGateway)
|
||||
|
||||
@@ -79,7 +79,7 @@ func (app *WebApp) Mount(r chi.Router, discoveryService *discovery.UnifiedDiscov
|
||||
// Custom URL playback
|
||||
r.Post("/api/play-url/{id}", app.HandlePlayURL)
|
||||
|
||||
// Text-to-speech (proxied to the AfterTouch service's /mgmt/tts/speak)
|
||||
// Text-to-speech (proxied to the AfterTouch service's /setup/tts/speak)
|
||||
r.Post("/api/device-speak/{id}", app.HandleAPISpeakText)
|
||||
|
||||
// SPA routes — serve index.html for client-side routing
|
||||
|
||||
Reference in New Issue
Block a user