mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
remove: proxy forwarding to Bose upstream
This commit is contained in:
committed by
Tobias Gesellchen
parent
0f0a96c0ce
commit
2747d95a8f
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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("<payload/>"))
|
||||
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("<secret/>"))
|
||||
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("<forwarded/>"))
|
||||
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 != "<forwarded/>" {
|
||||
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("<response>ok</response>"))
|
||||
}))
|
||||
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 := "<request>data</request>"
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user