From ffe61dd7a6760a3c5d4d4067f5503d3ad80e3f74 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Fri, 17 Apr 2026 18:20:46 +0200 Subject: [PATCH] Prevent loops for proxied requests on unknown endpoints (#166) Follow-up for https://github.com/gesellix/Bose-SoundTouch/issues/161 --- pkg/service/handlers/handlers_proxy.go | 16 ++++++ pkg/service/handlers/loop_prevention_test.go | 53 ++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 pkg/service/handlers/loop_prevention_test.go diff --git a/pkg/service/handlers/handlers_proxy.go b/pkg/service/handlers/handlers_proxy.go index 543786e..95f6771 100644 --- a/pkg/service/handlers/handlers_proxy.go +++ b/pkg/service/handlers/handlers_proxy.go @@ -15,6 +15,13 @@ import ( // 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) @@ -65,6 +72,8 @@ func (s *Server) ServeProxy(target *url.URL) http.HandlerFunc { pr.Out.URL.Path = target.Path } + pr.Out.Header.Set("X-Bose-Proxy-Hop", "1") + lp.LogRequest(pr.Out) }, Transport: &http.Transport{ @@ -101,6 +110,13 @@ func (s *Server) HandleNotFound(w http.ResponseWriter, r *http.Request) { // 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" diff --git a/pkg/service/handlers/loop_prevention_test.go b/pkg/service/handlers/loop_prevention_test.go new file mode 100644 index 0000000..c92be62 --- /dev/null +++ b/pkg/service/handlers/loop_prevention_test.go @@ -0,0 +1,53 @@ +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) + } + }) +}