Implement skip mirror endpoints to reduce false positives in parity checks (#126)

Added 'Skip Mirror Endpoints' setting to allow specific requests like
`/oauth/device/*/music/musicprovider/15/token/cs3` to be handled
exclusively locally, even when mirroring is enabled. Updated
MirrorMiddleware to check against the skip list before performing
mirroring or parity logic. Exposed the setting via the Web UI Settings
tab and the CLI. Updated relevant tests to accommodate the configuration
changes.

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
Tobias Gesellchen
2026-03-22 10:53:36 +01:00
committed by GitHub
co-authored by Junie
parent d5d6585517
commit 9f7cb81b45
13 changed files with 136 additions and 89 deletions
+25 -15
View File
@@ -189,6 +189,11 @@ func main() {
Usage: "Endpoints to mirror to Bose Cloud (comma-separated or multiple flags)",
EnvVars: []string{"MIRROR_ENDPOINTS"},
},
&cli.StringSliceFlag{
Name: "skip-mirror-endpoints",
Usage: "Endpoints to skip mirroring to Bose Cloud (comma-separated or multiple flags)",
EnvVars: []string{"SKIP_MIRROR_ENDPOINTS"},
},
&cli.StringSliceFlag{
Name: "internal-paths",
Usage: "Paths for internal requests (comma-separated or multiple flags)",
@@ -241,7 +246,7 @@ func main() {
server.SetVersionInfo(version, commit, date)
server.SetDiscoverySettings(config.discoveryInterval, persisted.DiscoveryEnabled)
server.SetDNSSettings(persisted.DNSEnabled, strings.Join(persisted.DNSUpstream, ","), persisted.DNSBindAddr)
server.SetMirrorSettings(persisted.MirrorEnabled, persisted.MirrorEndpoints, persisted.PreferredSource)
server.SetMirrorSettings(persisted.MirrorEnabled, persisted.MirrorEndpoints, persisted.SkipMirrorEndpoints, persisted.PreferredSource)
server.SetInternalPaths(persisted.InternalPaths)
server.SetSpotifyConfig(config.spotifyClientID, config.spotifyClientSecret, config.spotifyRedirectURI)
server.SetMgmtConfig(config.mgmtUsername, config.mgmtPassword)
@@ -374,6 +379,7 @@ type serviceConfig struct {
dnsBind string
mirrorEnabled bool
mirrorEndpoints []string
skipMirrorEndpoints []string
internalPaths []string
discoveryInterval time.Duration
domains []string
@@ -448,6 +454,7 @@ func loadConfig(c *cli.Context) serviceConfig {
mgmtPassword := c.String("mgmt-password")
mirrorEnabled := c.Bool("mirror-enabled")
mirrorEndpoints := c.StringSlice("mirror-endpoints")
skipMirrorEndpoints := c.StringSlice("skip-mirror-endpoints")
internalPaths := c.StringSlice("internal-paths")
migrationEnabled := c.Bool("migration-enabled")
migrationDryRun := c.Bool("migration-dry-run")
@@ -469,6 +476,7 @@ func loadConfig(c *cli.Context) serviceConfig {
dnsBind: dnsBind,
mirrorEnabled: mirrorEnabled,
mirrorEndpoints: mirrorEndpoints,
skipMirrorEndpoints: skipMirrorEndpoints,
internalPaths: internalPaths,
discoveryInterval: discoveryInterval,
domains: domains,
@@ -565,6 +573,7 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
config.mirrorEnabled = persisted.MirrorEnabled
config.mirrorEndpoints = persisted.MirrorEndpoints
config.skipMirrorEndpoints = persisted.SkipMirrorEndpoints
config.preferredSource = persisted.PreferredSource
config.internalPaths = persisted.InternalPaths
@@ -573,20 +582,21 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
func createDefaultSettings(ds *datastore.DataStore, config serviceConfig) datastore.Settings {
settings := datastore.Settings{
ServerURL: config.serverURL,
HTTPServerURL: config.httpsServerURL,
RedactLogs: config.redact,
LogBodies: config.logBody,
RecordInteractions: config.record,
DiscoveryInterval: config.discoveryInterval.String(),
DiscoveryEnabled: true,
DNSEnabled: config.dnsEnabled,
DNSUpstream: strings.Split(config.dnsUpstream, ","),
DNSBindAddr: config.dnsBind,
MirrorEnabled: config.mirrorEnabled,
MirrorEndpoints: config.mirrorEndpoints,
PreferredSource: config.preferredSource,
InternalPaths: config.internalPaths,
ServerURL: config.serverURL,
HTTPServerURL: config.httpsServerURL,
RedactLogs: config.redact,
LogBodies: config.logBody,
RecordInteractions: config.record,
DiscoveryInterval: config.discoveryInterval.String(),
DiscoveryEnabled: true,
DNSEnabled: config.dnsEnabled,
DNSUpstream: strings.Split(config.dnsUpstream, ","),
DNSBindAddr: config.dnsBind,
MirrorEnabled: config.mirrorEnabled,
MirrorEndpoints: config.mirrorEndpoints,
SkipMirrorEndpoints: config.skipMirrorEndpoints,
PreferredSource: config.preferredSource,
InternalPaths: config.internalPaths,
Shortcuts: map[string]int{
"/.well-known/appspecific/com.chrome.devtools.json": http.StatusNotFound,
"/sw.js": http.StatusNotFound,
+16 -15
View File
@@ -1138,21 +1138,22 @@ func (ds *DataStore) GetETagForAccount(account, device string) int64 {
// Settings represents the global service settings.
type Settings struct {
ServerURL string `json:"server_url"`
HTTPServerURL string `json:"https_server_url,omitempty"`
RedactLogs bool `json:"redact_logs"`
LogBodies bool `json:"log_bodies"`
RecordInteractions bool `json:"record_interactions"`
DiscoveryInterval string `json:"discovery_interval,omitempty"`
DiscoveryEnabled bool `json:"discovery_enabled"`
DNSEnabled bool `json:"dns_enabled"`
DNSUpstream []string `json:"dns_upstream,omitempty"`
DNSBindAddr string `json:"dns_bind_addr,omitempty"`
MirrorEnabled bool `json:"mirror_enabled"`
MirrorEndpoints []string `json:"mirror_endpoints,omitempty"`
PreferredSource string `json:"preferred_source,omitempty"`
InternalPaths []string `json:"internal_paths,omitempty"`
Shortcuts map[string]int `json:"shortcuts,omitempty"`
ServerURL string `json:"server_url"`
HTTPServerURL string `json:"https_server_url,omitempty"`
RedactLogs bool `json:"redact_logs"`
LogBodies bool `json:"log_bodies"`
RecordInteractions bool `json:"record_interactions"`
DiscoveryInterval string `json:"discovery_interval,omitempty"`
DiscoveryEnabled bool `json:"discovery_enabled"`
DNSEnabled bool `json:"dns_enabled"`
DNSUpstream []string `json:"dns_upstream,omitempty"`
DNSBindAddr string `json:"dns_bind_addr,omitempty"`
MirrorEnabled bool `json:"mirror_enabled"`
MirrorEndpoints []string `json:"mirror_endpoints,omitempty"`
SkipMirrorEndpoints []string `json:"skip_mirror_endpoints,omitempty"`
PreferredSource string `json:"preferred_source,omitempty"`
InternalPaths []string `json:"internal_paths,omitempty"`
Shortcuts map[string]int `json:"shortcuts,omitempty"`
}
// GetSettings retrieves the global service settings.
+1 -1
View File
@@ -81,7 +81,7 @@ func TestHandleBoseSpotifyToken_FallbackToProxy(t *testing.T) {
// Mirroring must be enabled for HandleBoseProxy to work (based on previous changes)
// Actually I reverted that, so it should work regardless of MirrorEnabled now.
server.SetMirrorSettings(true, nil, "")
server.SetMirrorSettings(true, nil, nil, "")
// chi.URLParam works when using chi router
r := chi.NewRouter()
+49 -44
View File
@@ -156,6 +156,7 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
dnsBindAddr := s.dnsBindAddr
mirrorEnabled := s.mirrorEnabled
mirrorEndpoints := s.mirrorEndpoints
skipMirrorEndpoints := s.skipMirrorEndpoints
preferredSource := s.preferredSource
internalPaths := s.internalPaths
redact, logBody, record := s.proxyRedact, s.proxyLogBody, s.recordEnabled
@@ -166,24 +167,25 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
dnsRunning, actualBind := s.GetDNSRunning()
if err := json.NewEncoder(w).Encode(map[string]interface{}{
"server_url": serverURL,
"https_server_url": httpsServerURL,
"discovery_interval": discoveryInterval,
"discovery_enabled": discoveryEnabled,
"dns_enabled": dnsEnabled,
"dns_running": dnsRunning,
"dns_actual_bind": actualBind,
"dns_upstream": strings.Join(dnsUpstream, ","),
"dns_bind_addr": dnsBindAddr,
"mirror_enabled": mirrorEnabled,
"mirror_endpoints": mirrorEndpoints,
"preferred_source": preferredSource,
"internal_paths": internalPaths,
"redact_logs": redact,
"log_bodies": logBody,
"record_interactions": record,
"shortcuts": shortcuts,
"spotify_configured": spotifyConfigured,
"server_url": serverURL,
"https_server_url": httpsServerURL,
"discovery_interval": discoveryInterval,
"discovery_enabled": discoveryEnabled,
"dns_enabled": dnsEnabled,
"dns_running": dnsRunning,
"dns_actual_bind": actualBind,
"dns_upstream": strings.Join(dnsUpstream, ","),
"dns_bind_addr": dnsBindAddr,
"mirror_enabled": mirrorEnabled,
"mirror_endpoints": mirrorEndpoints,
"skip_mirror_endpoints": skipMirrorEndpoints,
"preferred_source": preferredSource,
"internal_paths": internalPaths,
"redact_logs": redact,
"log_bodies": logBody,
"record_interactions": record,
"shortcuts": shortcuts,
"spotify_configured": spotifyConfigured,
}); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
@@ -193,17 +195,18 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
// HandleUpdateSettings updates the service settings.
func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
var settings struct {
ServerURL string `json:"server_url"`
DiscoveryInterval string `json:"discovery_interval"`
DiscoveryEnabled bool `json:"discovery_enabled"`
DNSEnabled bool `json:"dns_enabled"`
DNSUpstream string `json:"dns_upstream"`
DNSBindAddr string `json:"dns_bind_addr"`
MirrorEnabled bool `json:"mirror_enabled"`
MirrorEndpoints []string `json:"mirror_endpoints"`
PreferredSource string `json:"preferred_source"`
InternalPaths []string `json:"internal_paths"`
Shortcuts map[string]int `json:"shortcuts"`
ServerURL string `json:"server_url"`
DiscoveryInterval string `json:"discovery_interval"`
DiscoveryEnabled bool `json:"discovery_enabled"`
DNSEnabled bool `json:"dns_enabled"`
DNSUpstream string `json:"dns_upstream"`
DNSBindAddr string `json:"dns_bind_addr"`
MirrorEnabled bool `json:"mirror_enabled"`
MirrorEndpoints []string `json:"mirror_endpoints"`
SkipMirrorEndpoints []string `json:"skip_mirror_endpoints"`
PreferredSource string `json:"preferred_source"`
InternalPaths []string `json:"internal_paths"`
Shortcuts map[string]int `json:"shortcuts"`
}
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -249,6 +252,7 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
s.mirrorEnabled = settings.MirrorEnabled
s.mirrorEndpoints = settings.MirrorEndpoints
s.skipMirrorEndpoints = settings.SkipMirrorEndpoints
s.preferredSource = settings.PreferredSource
s.internalPaths = settings.InternalPaths
@@ -269,21 +273,22 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
log.Printf("Saving updated settings to %s/settings.json", s.ds.DataDir)
err = s.ds.SaveSettings(datastore.Settings{
ServerURL: s.serverURL,
HTTPServerURL: currentHTTPS,
RedactLogs: currentRedact,
LogBodies: currentLogBody,
RecordInteractions: currentRecord,
DiscoveryInterval: s.discoveryInterval.String(),
DiscoveryEnabled: s.discoveryEnabled,
DNSEnabled: s.dnsEnabled,
DNSUpstream: s.dnsUpstream,
DNSBindAddr: s.dnsBindAddr,
MirrorEnabled: s.mirrorEnabled,
MirrorEndpoints: s.mirrorEndpoints,
PreferredSource: s.preferredSource,
InternalPaths: s.internalPaths,
Shortcuts: s.shortcuts,
ServerURL: s.serverURL,
HTTPServerURL: currentHTTPS,
RedactLogs: currentRedact,
LogBodies: currentLogBody,
RecordInteractions: currentRecord,
DiscoveryInterval: s.discoveryInterval.String(),
DiscoveryEnabled: s.discoveryEnabled,
DNSEnabled: s.dnsEnabled,
DNSUpstream: s.dnsUpstream,
DNSBindAddr: s.dnsBindAddr,
MirrorEnabled: s.mirrorEnabled,
MirrorEndpoints: s.mirrorEndpoints,
SkipMirrorEndpoints: s.skipMirrorEndpoints,
PreferredSource: s.preferredSource,
InternalPaths: s.internalPaths,
Shortcuts: s.shortcuts,
})
dnsEnabled := s.dnsEnabled
@@ -20,7 +20,7 @@ func TestMirrorMiddleware_InfiniteLoop(t *testing.T) {
_ = ds.Initialize()
server := NewServer(ds, nil, "http://localhost:8000", false, false, false)
server.SetMirrorSettings(true, []string{"/loop"}, "upstream")
server.SetMirrorSettings(true, []string{"/loop"}, nil, "upstream")
// Create a handler that would be the "next" in the chain.
// If the loop occurs, this will be called repeatedly.
+14 -4
View File
@@ -26,10 +26,10 @@ import (
// MirrorMiddleware returns a middleware that mirrors specific requests to the Bose upstream.
func (s *Server) MirrorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
enabled, endpoints, preferredSource := s.getMirrorSettings()
enabled, endpoints, skipEndpoints, preferredSource := s.getMirrorSettings()
isMirrorRequest := r.Header.Get("X-Mirror-Request") == "true"
if !enabled || isMirrorRequest || len(endpoints) == 0 || !s.shouldMirror(r.URL.Path, endpoints) {
if !enabled || isMirrorRequest || len(endpoints) == 0 || !s.shouldMirror(r.URL.Path, endpoints) || s.shouldSkipMirror(r.URL.Path, skipEndpoints) {
next.ServeHTTP(w, r)
return
}
@@ -64,11 +64,11 @@ func (s *Server) MirrorMiddleware(next http.Handler) http.Handler {
})
}
func (s *Server) getMirrorSettings() (bool, []string, string) {
func (s *Server) getMirrorSettings() (bool, []string, []string, string) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.mirrorEnabled, s.mirrorEndpoints, s.preferredSource
return s.mirrorEnabled, s.mirrorEndpoints, s.skipMirrorEndpoints, s.preferredSource
}
func (s *Server) shouldMirror(path string, endpoints []string) bool {
@@ -81,6 +81,16 @@ func (s *Server) shouldMirror(path string, endpoints []string) bool {
return false
}
func (s *Server) shouldSkipMirror(path string, skipEndpoints []string) bool {
for _, pattern := range skipEndpoints {
if matchPattern(pattern, path) {
return true
}
}
return false
}
func (s *Server) mirrorUpstreamPreferred(detachedCtx context.Context, w http.ResponseWriter, r *http.Request, next http.Handler, bodyBytes []byte) {
log.Printf("[MIRROR] Upstream is preferred source for %s %s", r.Method, r.URL.Path)
@@ -40,7 +40,7 @@ func TestMirrorMiddleware_PreferredSource(t *testing.T) {
// 3. Setup our server with MirrorMiddleware
server := NewServer(ds, nil, "http://localhost:8000", false, false, false)
server.SetMirrorSettings(true, []string{"/test/local"}, "local")
server.SetMirrorSettings(true, []string{"/test/local"}, nil, "local")
// We need to trick performMirror to use our mock upstream.
// performMirror uses r.Host.
@@ -50,7 +50,7 @@ func TestMirrorMiddleware_PreferredSource(t *testing.T) {
middleware := server.MirrorMiddleware(r)
t.Run("PreferredLocal", func(t *testing.T) {
server.SetMirrorSettings(true, []string{"/test/local"}, "local")
server.SetMirrorSettings(true, []string{"/test/local"}, nil, "local")
req := httptest.NewRequest("GET", "/test/local", nil)
req.Host = upstreamHost // So performMirror targets the mock upstream
@@ -70,7 +70,7 @@ func TestMirrorMiddleware_PreferredSource(t *testing.T) {
})
t.Run("PreferredUpstream", func(t *testing.T) {
server.SetMirrorSettings(true, []string{"/test/local"}, "upstream")
server.SetMirrorSettings(true, []string{"/test/local"}, nil, "upstream")
req := httptest.NewRequest("GET", "/test/local", nil)
req.Host = upstreamHost
@@ -90,7 +90,7 @@ func TestMirrorMiddleware_PreferredSource(t *testing.T) {
})
t.Run("FallbackToLocal", func(t *testing.T) {
server.SetMirrorSettings(true, []string{"/test/local"}, "upstream")
server.SetMirrorSettings(true, []string{"/test/local"}, nil, "upstream")
// Use a non-existent host for mirror to trigger failure
req := httptest.NewRequest("GET", "/test/local", nil)
+2 -2
View File
@@ -45,7 +45,7 @@ func TestMirroring(t *testing.T) {
recorder := proxy.NewRecorder(tempDir)
server.SetRecorder(recorder)
server.SetRecordEnabled(true)
server.SetMirrorSettings(true, []string{"/streaming/account/*/device/*/recent"}, "local")
server.SetMirrorSettings(true, []string{"/streaming/account/*/device/*/recent"}, nil, "local")
ts := httptest.NewServer(r)
defer ts.Close()
@@ -167,7 +167,7 @@ func TestMirroring(t *testing.T) {
defer postUpstream.Close()
// Setup mirroring for the POST endpoint
server.SetMirrorSettings(true, []string{"/v1/scmudc/*"}, "local")
server.SetMirrorSettings(true, []string{"/v1/scmudc/*"}, nil, "local")
requestBody := `{"envelope":{"monoTime":234906,"payloadProtocolVersion":"3.1","payloadType":"scmudc","protocolVersion":"1.0","time":"2026-02-25T23:03:14.976349+00:00","uniqueId":"A81B6A536A98"},"payload":{"deviceInfo":{"boseID":"3230304","deviceID":"A81B6A536A98","deviceType":"SoundTouch 10","serialNumber":"I6332527703739342000020","softwareVersion":"27.0.6.46330.5043500 epdbuild.trunk.hepdswbld04.2022-08-04T11:20:29","systemSerialNumber":"069231P63364828AE"},"events":[{"data":{"play-state":"PAUSE_STATE"},"monoTime":234904,"time":"2026-02-25T23:03:14.973466+00:00","type":"play-state-changed"}]}}`
+3 -1
View File
@@ -40,6 +40,7 @@ type Server struct {
dnsBindAddr string
mirrorEnabled bool
mirrorEndpoints []string
skipMirrorEndpoints []string
preferredSource string
internalPaths []string
shortcuts map[string]int
@@ -319,12 +320,13 @@ func (s *Server) SetMgmtConfig(username, password string) {
}
// SetMirrorSettings sets the mirroring settings for the server.
func (s *Server) SetMirrorSettings(enabled bool, endpoints []string, preferredSource string) {
func (s *Server) SetMirrorSettings(enabled bool, endpoints, skipEndpoints []string, preferredSource string) {
s.mu.Lock()
defer s.mu.Unlock()
s.mirrorEnabled = enabled
s.mirrorEndpoints = endpoints
s.skipMirrorEndpoints = skipEndpoints
s.preferredSource = preferredSource
}
@@ -27,7 +27,7 @@ func TestSnapshotIntegrity_SelfAndMirror(t *testing.T) {
recorder := proxy.NewRecorder(tempDir)
s := NewServer(ds, nil, "http://localhost:8000", false, false, true)
s.SetRecorder(recorder)
s.SetMirrorSettings(true, []string{"/mirror/*"}, "local")
s.SetMirrorSettings(true, []string{"/mirror/*"}, nil, "local")
// Upstream mock
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -39,7 +39,7 @@ func TestSnapshotIntegrity_SelfAndMirror(t *testing.T) {
defer upstream.Close()
// Configure mirror to point to our mock upstream
s.SetMirrorSettings(true, []string{"/mirror/*"}, "local")
s.SetMirrorSettings(true, []string{"/mirror/*"}, nil, "local")
// We need to override the host in performMirror but for tests we can just mock it via env if needed or rely on the fact that performMirror uses r.Host
handler := s.SnapshotMiddleware(s.MirrorMiddleware(s.RecordMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+7
View File
@@ -228,6 +228,13 @@
style="width: 100%; max-width: 600px; margin-top: 5px; font-family: monospace;"
placeholder="/streaming/account/*/device/*/recent&#10;/accounts/*/devices/*/presets/*"
></textarea>
<label for="skip-mirror-endpoints" style="display: block; margin-top: 10px;">Skip Mirror Endpoints (local only, one per line):</label>
<textarea
id="skip-mirror-endpoints"
rows="2"
style="width: 100%; max-width: 600px; margin-top: 5px; font-family: monospace;"
placeholder="/oauth/device/*/music/musicprovider/*/token/cs3&#10;/oauth/device/*/music/musicprovider/*/token"
></textarea>
<div
class="info-box"
style="
+8
View File
@@ -158,6 +158,9 @@ async function fetchSettings() {
if (settings.mirror_endpoints) {
document.getElementById("mirror-endpoints").value = settings.mirror_endpoints.join("\n");
}
if (settings.skip_mirror_endpoints) {
document.getElementById("skip-mirror-endpoints").value = settings.skip_mirror_endpoints.join("\n");
}
if (settings.internal_paths) {
document.getElementById("internal-paths").value = settings.internal_paths.join("\n");
}
@@ -220,6 +223,11 @@ async function updateSettings() {
.value.split("\n")
.map((s) => s.trim())
.filter((s) => s !== ""),
skip_mirror_endpoints: document
.getElementById("skip-mirror-endpoints")
.value.split("\n")
.map((s) => s.trim())
.filter((s) => s !== ""),
internal_paths: document
.getElementById("internal-paths")
.value.split("\n")
+4
View File
@@ -73,6 +73,8 @@ type MigrationSummary struct {
IsMigrated bool `json:"is_migrated"`
MirrorEnabled bool `json:"mirror_enabled"`
MirrorEndpoints []string `json:"mirror_endpoints,omitempty"`
SkipMirrorEndpoints []string `json:"skip_mirror_endpoints,omitempty"`
PreferredSource string `json:"preferred_source,omitempty"`
}
// SSHClient defines the interface for SSH operations.
@@ -323,6 +325,8 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
if err == nil {
summary.MirrorEnabled = settings.MirrorEnabled
summary.MirrorEndpoints = settings.MirrorEndpoints
summary.SkipMirrorEndpoints = settings.SkipMirrorEndpoints
summary.PreferredSource = settings.PreferredSource
}
}