From 2747d95a8fdfae2ade8fcb5920a035166dbd803c Mon Sep 17 00:00:00 2001 From: Marcin Mennemann Date: Sun, 17 May 2026 15:10:08 +0200 Subject: [PATCH] remove: proxy forwarding to Bose upstream --- cmd/soundtouch-service/main.go | 3 - .../testdata/router_routes.txt | 10 -- pkg/service/datastore/datastore.go | 6 - pkg/service/handlers/handlers_oauth.go | 35 ++--- pkg/service/handlers/handlers_oauth_test.go | 28 +--- pkg/service/handlers/handlers_proxy.go | 138 +----------------- pkg/service/handlers/handlers_proxy_test.go | 119 +-------------- pkg/service/handlers/loop_prevention_test.go | 53 ------- pkg/service/handlers/server.go | 1 - 9 files changed, 32 insertions(+), 361 deletions(-) delete mode 100644 pkg/service/handlers/loop_prevention_test.go diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index 75fab3e..8219652 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -1046,7 +1046,6 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) * r.Post("/account/{account}/music/musicprovider/{sourceID}/token/cs", server.HandleBoseAccountToken) r.Post("/device/{deviceID}/music/musicprovider/{sourceID}/token/cs1", server.HandleBoseToken) r.Post("/device/{deviceID}/music/musicprovider/{sourceID}/token/cs3", server.HandleBoseToken) - r.HandleFunc("/*", server.HandleBoseProxy) }) r.Route("/v1", func(r chi.Router) { @@ -1098,8 +1097,6 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) * }) }) - r.Get("/proxy/*", server.HandleProxyRequest) - r.Route("/setup", func(r chi.Router) { r.Get("/devices", server.HandleListDiscoveredDevices) r.Post("/devices", server.HandleAddManualDevice) diff --git a/cmd/soundtouch-service/testdata/router_routes.txt b/cmd/soundtouch-service/testdata/router_routes.txt index 68baf17..3e499b3 100644 --- a/cmd/soundtouch-service/testdata/router_routes.txt +++ b/cmd/soundtouch-service/testdata/router_routes.txt @@ -1,12 +1,10 @@ CONNECT /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm CONNECT /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -CONNECT /oauth/* handlers.(*Server).HandleBoseProxy-fm DELETE /accounts/{account}/devices/{device} handlers.(*Server).HandleMargeRemoveDevice-fm DELETE /accounts/{account}/group/{groupId} handlers.(*Server).HandleMargeDeleteGroup-fm DELETE /bmx/tunein/v1/favorite/{stationID} handlers.(*Server).HandleTuneInDeleteFavorite-fm DELETE /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm DELETE /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -DELETE /oauth/* handlers.(*Server).HandleBoseProxy-fm DELETE /setup/devices/{deviceId} handlers.(*Server).HandleRemoveDevice-fm DELETE /setup/dns-discoveries handlers.(*Server).HandleClearDNSDiscoveries-fm DELETE /setup/interactions/sessions handlers.(*Server).HandleCleanupSessions-fm @@ -53,8 +51,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 /oauth/* handlers.(*Server).HandleBoseProxy-fm -GET /proxy/* handlers.(*Server).HandleProxyRequest-fm GET /setup/account-id-suggestions/{deviceId} handlers.(*Server).HandleAccountIDSuggestions-fm GET /setup/ca.crt handlers.(*Server).HandleGetCACert-fm GET /setup/devices handlers.(*Server).HandleListDiscoveredDevices-fm @@ -95,13 +91,10 @@ GET /v1/blacklist/{deviceId} setupRoute GET /web/* setupRouter.(*Server).HandleWeb HEAD /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm HEAD /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -HEAD /oauth/* handlers.(*Server).HandleBoseProxy-fm OPTIONS /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm OPTIONS /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -OPTIONS /oauth/* handlers.(*Server).HandleBoseProxy-fm PATCH /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm PATCH /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -PATCH /oauth/* handlers.(*Server).HandleBoseProxy-fm POST /accounts/{account}/devices handlers.(*Server).HandleMargeAddDevice-fm POST /accounts/{account}/devices/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm POST /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleMargeAddRecent-fm @@ -126,7 +119,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 /oauth/* handlers.(*Server).HandleBoseProxy-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 @@ -169,9 +161,7 @@ POST /v1/scmudc/{deviceId} handlers.( POST /v1/stapp/{deviceId} handlers.(*Server).HandleAppEvents-fm PUT /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm PUT /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -PUT /oauth/* handlers.(*Server).HandleBoseProxy-fm PUT /streaming/account/{account}/device/{device} handlers.(*Server).HandleMargeUpdateDevice-fm PUT /streaming/account/{account}/device/{device}/preset/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm TRACE /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter handlers.(*Server).HandleSiriusXMLiveAdapter-fm TRACE /core02/svc-bmx-adapter-siriusxm-everest-eco1/prod/live-adapter/* handlers.(*Server).HandleSiriusXMLiveAdapterSubpath-fm -TRACE /oauth/* handlers.(*Server).HandleBoseProxy-fm diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go index ddb87a3..6c29f27 100644 --- a/pkg/service/datastore/datastore.go +++ b/pkg/service/datastore/datastore.go @@ -2102,12 +2102,6 @@ type Settings struct { AmazonClientSecret string `json:"amazon_client_secret,omitempty"` AmazonRedirectURI string `json:"amazon_redirect_uri,omitempty"` - // AllowInsecureUpstreamTLS, when true, disables TLS certificate verification - // for the upstream Bose-cloud proxy traffic. The default (false) - // keeps verification on; opt in only when the upstream certificate chain is - // broken (post end-of-service) and a temporary unblock is required. - AllowInsecureUpstreamTLS bool `json:"allow_insecure_upstream_tls,omitempty"` - // TrustForwardedHeaders enables proxy-aware client IP resolution: when the // immediate TCP peer is one of the TrustedProxyCIDRs, the X-Real-IP / // X-Forwarded-For / True-Client-IP headers are honoured and replace diff --git a/pkg/service/handlers/handlers_oauth.go b/pkg/service/handlers/handlers_oauth.go index 71133ed..e6180dc 100644 --- a/pkg/service/handlers/handlers_oauth.go +++ b/pkg/service/handlers/handlers_oauth.go @@ -35,7 +35,8 @@ func (s *Server) HandleBoseToken(w http.ResponseWriter, r *http.Request) { } } - s.HandleBoseProxy(w, r) + log.Printf("[OAuth] Unknown music provider: %s", sourceID) + http.Error(w, "Unknown music provider", http.StatusNotFound) } // HandleBoseLegacyToken handles the Bose-specific token refresh request (legacy or variant). @@ -111,16 +112,16 @@ func (s *Server) HandleBoseAmazonToken(w http.ResponseWriter, r *http.Request) { s.mu.RUnlock() if svc == nil { - log.Printf("[Amazon] Amazon service not configured, falling back to upstream") - s.HandleBoseProxy(w, r) + log.Printf("[Amazon] Amazon service not configured, returning 503") + http.Error(w, "Amazon service not configured", http.StatusServiceUnavailable) return } accounts := svc.GetAccounts() if len(accounts) == 0 { - log.Printf("[Amazon] No Amazon accounts linked, falling back to upstream") - s.HandleBoseProxy(w, r) + log.Printf("[Amazon] No Amazon accounts linked, returning 503") + http.Error(w, "No linked Amazon accounts", http.StatusServiceUnavailable) return } @@ -157,8 +158,8 @@ func (s *Server) HandleBoseAmazonToken(w http.ResponseWriter, r *http.Request) { if account != nil { if err := svc.RefreshAccessToken(account); err != nil { - log.Printf("[Amazon] Failed to refresh token for %s: %v. Falling back to upstream", account.UserID, err) - s.HandleBoseProxy(w, r) + log.Printf("[Amazon] Failed to refresh token for %s: %v. Returning 502", account.UserID, err) + http.Error(w, "Token refresh failed", http.StatusBadGateway) return } @@ -169,8 +170,8 @@ func (s *Server) HandleBoseAmazonToken(w http.ResponseWriter, r *http.Request) { accessToken, userID, err = svc.GetFreshToken() if err != nil { - log.Printf("[Amazon] Failed to get fresh token: %v. Falling back to upstream", err) - s.HandleBoseProxy(w, r) + log.Printf("[Amazon] Failed to get fresh token: %v. Returning 502", err) + http.Error(w, "Failed to get fresh token", http.StatusBadGateway) return } @@ -206,16 +207,16 @@ func (s *Server) HandleBoseSpotifyToken(w http.ResponseWriter, r *http.Request) s.mu.RUnlock() if svc == nil { - log.Printf("[Spotify Proxy] Spotify service not configured, falling back to upstream") - s.HandleBoseProxy(w, r) + log.Printf("[Spotify Proxy] Spotify service not configured, returning 503") + http.Error(w, "Spotify service not configured", http.StatusServiceUnavailable) return } accounts := svc.GetAccounts() if len(accounts) == 0 { - log.Printf("[Spotify Proxy] No Spotify accounts linked, falling back to upstream") - s.HandleBoseProxy(w, r) + log.Printf("[Spotify Proxy] No Spotify accounts linked, returning 503") + http.Error(w, "No linked Spotify accounts", http.StatusServiceUnavailable) return } @@ -256,8 +257,8 @@ func (s *Server) HandleBoseSpotifyToken(w http.ResponseWriter, r *http.Request) if account != nil { if err := svc.RefreshAccessToken(account); err != nil { - log.Printf("[Spotify Proxy] Failed to refresh token for %s: %v. Falling back to upstream", account.UserID, err) - s.HandleBoseProxy(w, r) + log.Printf("[Spotify Proxy] Failed to refresh token for %s: %v. Returning 502", account.UserID, err) + http.Error(w, "Token refresh failed", http.StatusBadGateway) return } @@ -269,8 +270,8 @@ func (s *Server) HandleBoseSpotifyToken(w http.ResponseWriter, r *http.Request) accessToken, userID, err = svc.GetFreshToken() if err != nil { - log.Printf("[Spotify Proxy] Failed to get fresh token: %v. Falling back to upstream", err) - s.HandleBoseProxy(w, r) + log.Printf("[Spotify Proxy] Failed to get fresh token: %v. Returning 502", err) + http.Error(w, "Failed to get fresh token", http.StatusBadGateway) return } diff --git a/pkg/service/handlers/handlers_oauth_test.go b/pkg/service/handlers/handlers_oauth_test.go index a5bcd7c..77ec4fe 100644 --- a/pkg/service/handlers/handlers_oauth_test.go +++ b/pkg/service/handlers/handlers_oauth_test.go @@ -84,33 +84,17 @@ func TestHandleBoseSpotifyToken_FallbackToProxy(t *testing.T) { ds := datastore.NewDataStore(tmpDir) server := NewServer(ds, nil, "http://localhost", false, false, false) - // chi.URLParam works when using chi router r := chi.NewRouter() r.Post("/oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs3", server.HandleBoseToken) - // Since there's no Spotify service, it should fall back to HandleBoseProxy. - // HandleBoseProxy will try to contact streaming.bose.com. - // We can check if it returns a 502 or 404 (since we are not actually proxying to real Bose). - + // Without a configured Spotify service, the handler returns 503. req := httptest.NewRequest("POST", "/oauth/device/DEVICE123/music/musicprovider/15/token/cs3", nil) - req.Host = "localhost" // use localhost to avoid real network call w := httptest.NewRecorder() r.ServeHTTP(w, req) - // If it fell back to proxy, it should NOT have X-Proxy-Origin: self - if w.Header().Get("X-Proxy-Origin") == "self" { - t.Errorf("Expected fallback to proxy, but got X-Proxy-Origin: self") - } - - // HandleBoseProxy sets X-Proxy-Origin: upstream - if w.Header().Get("X-Proxy-Origin") != "upstream" { - // It might fail before setting the header if the target host is invalid, - // but our HandleBoseProxy sets it in ModifyResponse. - // If it fails to connect, it might return 502 without the header. - if w.Code != http.StatusBadGateway && w.Code != http.StatusNotFound { - t.Errorf("Expected fallback to proxy (upstream), got status %d and origin %s", w.Code, w.Header().Get("X-Proxy-Origin")) - } + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected 503 without Spotify service, got %d", w.Code) } } @@ -260,14 +244,14 @@ func TestHandleBoseAmazonToken_FallbackToProxy(t *testing.T) { r := chi.NewRouter() r.Post("/oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs1", server.HandleBoseToken) + // Without a configured Amazon service, the handler returns 503. req := httptest.NewRequest("POST", "/oauth/device/DEVICE123/music/musicprovider/20/token/cs1", nil) - req.Host = "localhost" w := httptest.NewRecorder() r.ServeHTTP(w, req) - if w.Header().Get("X-Proxy-Origin") == "self" { - t.Error("Expected fallback to proxy, but got X-Proxy-Origin: self") + if w.Code != http.StatusServiceUnavailable { + t.Errorf("expected 503 without Amazon service, got %d", w.Code) } } diff --git a/pkg/service/handlers/handlers_proxy.go b/pkg/service/handlers/handlers_proxy.go index ce03d6f..0ff00ba 100644 --- a/pkg/service/handlers/handlers_proxy.go +++ b/pkg/service/handlers/handlers_proxy.go @@ -2,114 +2,11 @@ package handlers import ( "bytes" - "crypto/tls" "io" "log" "net/http" - "net/http/httputil" - "net/url" - "strings" - - "github.com/gesellix/bose-soundtouch/pkg/service/proxy" ) -// HandleProxyRequest handles requests to the logging proxy. -func (s *Server) HandleProxyRequest(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("X-Bose-Proxy-Hop") != "" { - log.Printf("[PROXY_LOOP] Loop detected for %s %s, breaking loop", r.Method, r.URL.Path) - http.Error(w, "Loop detected", http.StatusNotFound) - - return - } - - targetURLStr := strings.TrimPrefix(r.URL.Path, "/proxy/") - if targetURLStr == "" { - http.Error(w, "Target URL is required", http.StatusBadRequest) - return - } - - // Reconstruct original URL (it might have lost its double slashes in the path) - if !strings.HasPrefix(targetURLStr, "http://") && !strings.HasPrefix(targetURLStr, "https://") { - // Try to fix it if it looks like http:/... - if strings.HasPrefix(targetURLStr, "http:/") { - targetURLStr = "http://" + strings.TrimPrefix(targetURLStr, "http:/") - } else if strings.HasPrefix(targetURLStr, "https:/") { - targetURLStr = "https://" + strings.TrimPrefix(targetURLStr, "https:/") - } - } - - target, err := url.Parse(targetURLStr) - if err != nil { - http.Error(w, "Invalid target URL: "+err.Error(), http.StatusBadRequest) - return - } - - s.ServeProxy(target)(w, r) -} - -// ServeProxy returns a handler that proxies to the given target. -func (s *Server) ServeProxy(target *url.URL) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - lp := proxy.NewLoggingProxy(target.String(), s.proxyRedact) - lp.LogBody = s.proxyLogBody - lp.RecordEnabled = s.recordEnabled - lp.SetRecorder(s.recorder) - - // Capture request body for recording, as it will be consumed by the proxy - var reqBody []byte - if r.Body != nil { - reqBody, _ = io.ReadAll(r.Body) - r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) - } - - // AllowInsecureUpstreamTLS is opt-in via settings.json — defaults to - // false so the upstream certificate chain is verified normally. The - // opt-in exists for deployments stuck behind a broken Bose-cloud - // chain post end-of-service. - settings, _ := s.ds.GetSettings() - insecure := settings.AllowInsecureUpstreamTLS - - rp := &httputil.ReverseProxy{ - Rewrite: func(pr *httputil.ProxyRequest) { - pr.SetURL(target) - pr.Out.Host = target.Host - // If target has a path, we should probably append or replace. - // For Bose upstream, it's usually just the domain. - if target.Path != "" && target.Path != "/" { - pr.Out.URL.Path = target.Path - } - - pr.Out.Header.Set("X-Bose-Proxy-Hop", "1") - - lp.LogRequest(pr.Out) - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, - }, - } - - rp.ModifyResponse = func(res *http.Response) error { - res.Header.Set("X-Proxy-Origin", "upstream") - // Generic Header Preservation - if etags, ok := res.Header["Etag"]; ok { - delete(res.Header, "Etag") - res.Header["ETag"] = etags - } - - // Restore captured request body for the recorder - if reqBody != nil { - res.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) - } - - lp.LogResponse(res) - - return nil - } - - rp.ServeHTTP(w, r) - } -} - // HandleNotFound handles requests that don't match any route. // It always logs [UNHANDLED] so unimplemented endpoints are visible in plain output. // When proxyLogBody is enabled it also logs the request body (truncated to 512 bytes). @@ -131,38 +28,5 @@ func (s *Server) HandleNotFound(w http.ResponseWriter, r *http.Request) { log.Printf("[UNHANDLED] %s %s", r.Method, r.URL.Path) } - s.HandleBoseProxy(w, r) -} - -// HandleBoseProxy proxies the request to the Bose upstream. -func (s *Server) HandleBoseProxy(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("X-Bose-Proxy-Hop") != "" { - log.Printf("[PROXY_LOOP] Loop detected for %s %s, breaking loop", r.Method, r.URL.Path) - http.Error(w, "Loop detected", http.StatusNotFound) - - return - } - - host := r.Host - if host == "" { - host = "streaming.bose.com" - } - - // Default to HTTPS for Bose services - scheme := "https" - if strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") || strings.HasPrefix(host, "::1") { - scheme = "http" - } - - targetURL := scheme + "://" + host - - target, err := url.Parse(targetURL) - if err != nil { - log.Printf("[PROXY_ERR] Failed to parse target URL %s: %v", targetURL, err) - http.Error(w, "Invalid upstream host", http.StatusBadGateway) - - return - } - - s.ServeProxy(target)(w, r) + http.NotFound(w, r) } diff --git a/pkg/service/handlers/handlers_proxy_test.go b/pkg/service/handlers/handlers_proxy_test.go index 00f94ac..8d1015b 100644 --- a/pkg/service/handlers/handlers_proxy_test.go +++ b/pkg/service/handlers/handlers_proxy_test.go @@ -2,28 +2,17 @@ package handlers import ( "bytes" - "io" "log" "net/http" "net/http/httptest" "os" - "path/filepath" "strings" "testing" "github.com/gesellix/bose-soundtouch/pkg/service/datastore" - "github.com/gesellix/bose-soundtouch/pkg/service/proxy" ) func TestHandleNotFound_UnhandledLogging(t *testing.T) { - // backend absorbs proxied requests so the test doesn't hit the real Bose upstream - backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - })) - defer backend.Close() - - backendHost := strings.TrimPrefix(backend.URL, "http://") - captureLog := func(fn func()) string { var buf bytes.Buffer log.SetOutput(&buf) @@ -37,7 +26,6 @@ func TestHandleNotFound_UnhandledLogging(t *testing.T) { server := NewServer(ds, nil, "http://localhost", false, false, false) req := httptest.NewRequest("GET", "/some/unknown/path", nil) - req.Host = backendHost logged := captureLog(func() { server.HandleNotFound(httptest.NewRecorder(), req) @@ -56,7 +44,6 @@ func TestHandleNotFound_UnhandledLogging(t *testing.T) { server := NewServer(ds, nil, "http://localhost", false, true, false) req := httptest.NewRequest("POST", "/marge/unknown", bytes.NewBufferString("")) - req.Host = backendHost logged := captureLog(func() { server.HandleNotFound(httptest.NewRecorder(), req) @@ -72,7 +59,6 @@ func TestHandleNotFound_UnhandledLogging(t *testing.T) { server := NewServer(ds, nil, "http://localhost", false, false, false) req := httptest.NewRequest("POST", "/marge/unknown", bytes.NewBufferString("")) - req.Host = backendHost logged := captureLog(func() { server.HandleNotFound(httptest.NewRecorder(), req) @@ -86,108 +72,17 @@ func TestHandleNotFound_UnhandledLogging(t *testing.T) { } }) - t.Run("body is still forwarded to proxy after being read for logging", func(t *testing.T) { - var receivedBody string - forwardCheck := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, _ := io.ReadAll(r.Body) - receivedBody = string(b) - w.WriteHeader(http.StatusOK) - })) - defer forwardCheck.Close() - + t.Run("returns 404 for unmatched routes", func(t *testing.T) { ds := datastore.NewDataStore(t.TempDir()) - server := NewServer(ds, nil, "http://localhost", false, true, false) + server := NewServer(ds, nil, "http://localhost", false, false, false) - req := httptest.NewRequest("POST", "/marge/unknown", bytes.NewBufferString("")) - req.Host = strings.TrimPrefix(forwardCheck.URL, "http://") + req := httptest.NewRequest("GET", "/some/unknown/path", nil) + w := httptest.NewRecorder() - captureLog(func() { - server.HandleNotFound(httptest.NewRecorder(), req) - }) + server.HandleNotFound(w, req) - if receivedBody != "" { - t.Errorf("expected body forwarded to proxy, got: %q", receivedBody) + if w.Code != http.StatusNotFound { + t.Errorf("expected 404 for unmatched route, got %d", w.Code) } }) } - -func TestHandleProxyRequest_RequestBodyRecording(t *testing.T) { - t.Setenv("RECORDER_ASYNC", "false") - tmpDir, err := os.MkdirTemp("", "proxy-request-body-test") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - // Start a backend server to receive the proxied request - backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Read the body to ensure it's consumed - _, _ = io.ReadAll(r.Body) - w.Header().Set("Content-Type", "application/xml") - w.WriteHeader(http.StatusOK) - w.Write([]byte("ok")) - })) - defer backend.Close() - - ds := datastore.NewDataStore(filepath.Join(tmpDir, "test.db")) - server := NewServer(ds, nil, "http://localhost", false, false, false) - server.recordEnabled = true - server.proxyLogBody = true - recorder := proxy.NewRecorder(tmpDir) - server.SetRecorder(recorder) - - // Create a proxy request to the backend - requestBody := "data" - targetURL := backend.URL - proxyPath := "/proxy/" + targetURL - req := httptest.NewRequest("POST", proxyPath, bytes.NewBufferString(requestBody)) - req.Header.Set("Content-Type", "application/xml") - - w := httptest.NewRecorder() - server.HandleProxyRequest(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Verify that the interaction was recorded and contains the request body - sessionID := recorder.SessionID - - // The recorder uses sanitized segments for the directory. - // Since the target URL is http://127.0.0.1:PORT, the path is empty, - // so it should be in the "root" directory under the category. - - // We'll search recursively to be sure - foundBody := false - err = filepath.Walk(filepath.Join(tmpDir, "interactions", sessionID), func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() && strings.HasSuffix(path, ".http") { - content, err := os.ReadFile(path) - if err != nil { - return err - } - if strings.Contains(string(content), requestBody) { - foundBody = true - } - } - return nil - }) - - if err != nil { - t.Fatalf("failed to walk interactions dir: %v", err) - } - - if !foundBody { - t.Errorf("request body %q not found in any recorded interaction file", requestBody) - // List all files found for debugging - _ = filepath.Walk(filepath.Join(tmpDir, "interactions", sessionID), func(path string, info os.FileInfo, err error) error { - if !info.IsDir() { - content, _ := os.ReadFile(path) - t.Logf("Found file %s with content:\n%s", path, string(content)) - } - return nil - }) - } -} diff --git a/pkg/service/handlers/loop_prevention_test.go b/pkg/service/handlers/loop_prevention_test.go deleted file mode 100644 index c92be62..0000000 --- a/pkg/service/handlers/loop_prevention_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package handlers - -import ( - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/gesellix/bose-soundtouch/pkg/service/datastore" -) - -func TestHandleBoseProxy_LoopPrevention(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "proxy-loop-test") - if err != nil { - t.Fatalf("failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - ds := datastore.NewDataStore(filepath.Join(tmpDir, "test.db")) - server := NewServer(ds, nil, "http://localhost", false, false, false) - - t.Run("first hop should be allowed", func(t *testing.T) { - req := httptest.NewRequest("GET", "/unknown-endpoint", nil) - req.Host = "localhost" - w := httptest.NewRecorder() - server.HandleBoseProxy(w, req) - if w.Code == http.StatusNotFound { - t.Errorf("Expected first hop to be allowed (even if it fails later), but got 404") - } - }) - - t.Run("second hop should be blocked", func(t *testing.T) { - req := httptest.NewRequest("GET", "/unknown-endpoint", nil) - req.Host = "localhost" - req.Header.Set("X-Bose-Proxy-Hop", "1") - w := httptest.NewRecorder() - server.HandleBoseProxy(w, req) - if w.Code != http.StatusNotFound { - t.Errorf("Expected second hop to be blocked with 404, but got %d", w.Code) - } - }) - - t.Run("HandleProxyRequest loop detection", func(t *testing.T) { - req := httptest.NewRequest("GET", "/proxy/http://example.com", nil) - req.Header.Set("X-Bose-Proxy-Hop", "1") - w := httptest.NewRecorder() - server.HandleProxyRequest(w, req) - if w.Code != http.StatusNotFound { - t.Errorf("Expected HandleProxyRequest loop to be blocked with 404, but got %d", w.Code) - } - }) -} diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index a42190f..be9a3ed 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -47,7 +47,6 @@ type Server struct { shortcuts map[string]int recorder *proxy.Recorder dnsDiscovery *discovery.DNSDiscovery - UpstreamProxy http.Handler Version string Commit string Date string