Remove Soundcork fallback

This commit is contained in:
Tobias Gesellchen
2026-02-26 22:26:35 +01:00
parent 6211e34050
commit 6ee0fc8115
24 changed files with 229 additions and 387 deletions
+68 -93
View File
@@ -82,17 +82,6 @@ func main() {
Usage: "Network interface to bind to",
EnvVars: []string{"BIND_ADDR"},
},
&cli.StringFlag{
Name: "soundcork-url",
Usage: "URL for Soundcork-based service components (legacy)",
Value: "http://localhost:8001",
EnvVars: []string{"SOUNDCORK_BACKEND_URL", "TARGET_URL"},
},
&cli.BoolFlag{
Name: "enable-soundcork-proxy",
Usage: "Enable proxying unknown requests to the Soundcork backend",
EnvVars: []string{"ENABLE_SOUNDCORK_PROXY"},
},
&cli.StringFlag{
Name: "data-dir",
Usage: "Directory for persistent data",
@@ -246,9 +235,8 @@ func main() {
sm := setup.NewManager(config.serverURL, ds, cm)
sm.MgmtUsername = config.mgmtUsername
sm.MgmtPassword = config.mgmtPassword
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record, config.enableSoundcorkProxy, config.migrationEnabled, config.migrationDryRun)
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record, config.migrationEnabled, config.migrationDryRun)
sm.GetDNSRunning = server.GetDNSRunning
server.SetSoundcorkURL(config.soundcorkURL)
server.SetHTTPServerURL(config.httpsServerURL)
server.SetVersionInfo(version, commit, date)
server.SetDiscoverySettings(config.discoveryInterval, persisted.DiscoveryEnabled)
@@ -337,7 +325,7 @@ func main() {
r := setupRouter(server)
log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.soundcorkURL)
log.Printf("Go service starting on %s", config.serverURL)
if tlsConfig != nil {
startHTTPSServer(config.httpsAddr, r, tlsConfig, config.httpsServerURL)
@@ -371,34 +359,32 @@ func showVersionInfo(_ *cli.Context) error {
}
type serviceConfig struct {
port string
bindAddr string
addr string
soundcorkURL string
dataDir string
serverURL string
httpsServerURL string
httpsAddr string
redact bool
logBody bool
record bool
enableSoundcorkProxy bool
dnsEnabled bool
dnsUpstream string
dnsBind string
mirrorEnabled bool
mirrorEndpoints []string
internalPaths []string
discoveryInterval time.Duration
domains []string
spotifyClientID string
spotifyClientSecret string
spotifyRedirectURI string
mgmtUsername string
mgmtPassword string
migrationEnabled bool
migrationDryRun bool
preferredSource string
port string
bindAddr string
addr string
dataDir string
serverURL string
httpsServerURL string
httpsAddr string
redact bool
logBody bool
record bool
dnsEnabled bool
dnsUpstream string
dnsBind string
mirrorEnabled bool
mirrorEndpoints []string
internalPaths []string
discoveryInterval time.Duration
domains []string
spotifyClientID string
spotifyClientSecret string
spotifyRedirectURI string
mgmtUsername string
mgmtPassword string
migrationEnabled bool
migrationDryRun bool
preferredSource string
}
func loadConfig(c *cli.Context) serviceConfig {
@@ -410,7 +396,6 @@ func loadConfig(c *cli.Context) serviceConfig {
addr = ":" + port
}
soundcorkURL := c.String("soundcork-url")
dataDir := c.String("data-dir")
hostname, _ := os.Hostname()
@@ -442,7 +427,6 @@ func loadConfig(c *cli.Context) serviceConfig {
redact := c.Bool("redact-logs")
logBody := c.Bool("log-bodies")
record := c.Bool("record-interactions")
enableSoundcorkProxy := c.Bool("enable-soundcork-proxy")
dnsEnabled := c.Bool("dns-discovery")
dnsUpstream := c.String("dns-upstream")
@@ -470,34 +454,32 @@ func loadConfig(c *cli.Context) serviceConfig {
preferredSource := c.String("preferred-source")
return serviceConfig{
port: port,
bindAddr: bindAddr,
addr: addr,
soundcorkURL: soundcorkURL,
dataDir: dataDir,
serverURL: serverURL,
httpsServerURL: httpsServerURL,
httpsAddr: httpsAddr,
redact: redact,
logBody: logBody,
record: record,
enableSoundcorkProxy: enableSoundcorkProxy,
dnsEnabled: dnsEnabled,
dnsUpstream: dnsUpstream,
dnsBind: dnsBind,
mirrorEnabled: mirrorEnabled,
mirrorEndpoints: mirrorEndpoints,
internalPaths: internalPaths,
discoveryInterval: discoveryInterval,
domains: domains,
spotifyClientID: spotifyClientID,
spotifyClientSecret: spotifyClientSecret,
spotifyRedirectURI: spotifyRedirectURI,
mgmtUsername: mgmtUsername,
mgmtPassword: mgmtPassword,
migrationEnabled: migrationEnabled,
migrationDryRun: migrationDryRun,
preferredSource: preferredSource,
port: port,
bindAddr: bindAddr,
addr: addr,
dataDir: dataDir,
serverURL: serverURL,
httpsServerURL: httpsServerURL,
httpsAddr: httpsAddr,
redact: redact,
logBody: logBody,
record: record,
dnsEnabled: dnsEnabled,
dnsUpstream: dnsUpstream,
dnsBind: dnsBind,
mirrorEnabled: mirrorEnabled,
mirrorEndpoints: mirrorEndpoints,
internalPaths: internalPaths,
discoveryInterval: discoveryInterval,
domains: domains,
spotifyClientID: spotifyClientID,
spotifyClientSecret: spotifyClientSecret,
spotifyRedirectURI: spotifyRedirectURI,
mgmtUsername: mgmtUsername,
mgmtPassword: mgmtPassword,
migrationEnabled: migrationEnabled,
migrationDryRun: migrationDryRun,
preferredSource: preferredSource,
}
}
@@ -558,10 +540,6 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
config.serverURL = persisted.ServerURL
}
if persisted.SoundcorkURL != "" {
config.soundcorkURL = persisted.SoundcorkURL
}
if persisted.HTTPServerURL != "" {
config.httpsServerURL = persisted.HTTPServerURL
}
@@ -575,7 +553,6 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
config.redact = persisted.RedactLogs
config.logBody = persisted.LogBodies
config.record = persisted.RecordInteractions
config.enableSoundcorkProxy = persisted.EnableSoundcorkProxy
config.dnsEnabled = persisted.DNSEnabled
if len(persisted.DNSUpstream) > 0 {
@@ -596,22 +573,20 @@ func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) data
func createDefaultSettings(ds *datastore.DataStore, config serviceConfig) datastore.Settings {
settings := datastore.Settings{
ServerURL: config.serverURL,
SoundcorkURL: config.soundcorkURL,
HTTPServerURL: config.httpsServerURL,
RedactLogs: config.redact,
LogBodies: config.logBody,
RecordInteractions: config.record,
DiscoveryInterval: config.discoveryInterval.String(),
DiscoveryEnabled: true,
EnableSoundcorkProxy: config.enableSoundcorkProxy,
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,
PreferredSource: config.preferredSource,
InternalPaths: config.internalPaths,
Shortcuts: map[string]int{
"/.well-known/appspecific/com.chrome.devtools.json": http.StatusNotFound,
"/sw.js": http.StatusNotFound,
+6 -11
View File
@@ -18,10 +18,9 @@ func TestApplyPersistedSettings(t *testing.T) {
t.Run("overrides true with false", func(t *testing.T) {
config := &serviceConfig{
redact: true,
logBody: true,
record: true,
enableSoundcorkProxy: true,
redact: true,
logBody: true,
record: true,
}
// Simulate the bug by using the old bitwise OR logic in the test,
@@ -29,10 +28,9 @@ func TestApplyPersistedSettings(t *testing.T) {
// config.redact = config.redact || false -> stays true
settings := datastore.Settings{
RedactLogs: false,
LogBodies: false,
RecordInteractions: false,
EnableSoundcorkProxy: false,
RedactLogs: false,
LogBodies: false,
RecordInteractions: false,
}
err := ds.SaveSettings(settings)
if err != nil {
@@ -50,9 +48,6 @@ func TestApplyPersistedSettings(t *testing.T) {
if config.record != false {
t.Errorf("Expected record to be false, got true")
}
if config.enableSoundcorkProxy != false {
t.Errorf("Expected enableSoundcorkProxy to be false, got true")
}
})
t.Run("retains false when settings are false", func(t *testing.T) {
+15 -17
View File
@@ -875,23 +875,21 @@ func (ds *DataStore) GetETagForAccount(account, device string) int64 {
// Settings represents the global service settings.
type Settings struct {
ServerURL string `json:"server_url"`
SoundcorkURL string `json:"soundcork_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"`
EnableSoundcorkProxy bool `json:"enable_soundcork_proxy"`
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"`
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
View File
@@ -382,7 +382,6 @@ func TestSettingsPersistence(t *testing.T) {
settings := Settings{
ServerURL: "http://myserver:8000",
SoundcorkURL: "http://myproxy:8001",
LogBodies: true,
DiscoveryInterval: "10m",
DiscoveryEnabled: true,
@@ -154,7 +154,7 @@ func TestComprehensiveMigration_MultipleExistingDevices(t *testing.T) {
deviceIP := server.URL[len("http://"):]
sm := setup.NewManager(server.URL, ds, nil)
srv := NewServer(ds, sm, server.URL, false, false, false, false, false, false)
srv := NewServer(ds, sm, server.URL, false, false, false, false, false)
// Simulate device rediscovery
discoveredDevice := models.DiscoveredDevice{
@@ -233,7 +233,7 @@ func TestFindAllExistingDeviceVariants_MatchingCriteria(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
accountID := "testaccount"
// Create devices that should match various criteria
@@ -344,7 +344,7 @@ func TestMigration_EdgeCases(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
accountID := "testaccount"
t.Run("NoExistingDevices", func(t *testing.T) {
+3 -3
View File
@@ -16,7 +16,7 @@ func TestDeviceMigration_DirectoryRename(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, true, false)
srv := NewServer(ds, nil, "http://localhost", false, false, false, true, false)
accountID := "test-account"
macAddress := "A81B6A536A98"
@@ -121,7 +121,7 @@ func TestDeviceMigration_NoExistingTarget(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, true, false)
srv := NewServer(ds, nil, "http://localhost", false, false, false, true, false)
accountID := "test-account"
macAddress := "A81B6A536A98"
@@ -189,7 +189,7 @@ func TestDeviceMigration_ExistingTargetRemoved(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
srv := NewServer(ds, nil, "http://localhost", false, false, false, false, true, false)
srv := NewServer(ds, nil, "http://localhost", false, false, false, true, false)
accountID := "test-account"
macAddress := "A81B6A536A98"
+1 -1
View File
@@ -14,7 +14,7 @@ import (
func TestEventLog(t *testing.T) {
ds := datastore.NewDataStore(t.TempDir())
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
r := chi.NewRouter()
r.Post("/streaming/stats/usage", s.HandleUsageStats)
+1 -1
View File
@@ -20,7 +20,7 @@ type healthResp struct {
func TestHealthEndpoint(t *testing.T) {
r := chi.NewRouter()
srv := NewServer(nil, nil, "http://localhost", false, false, false, false, false, false)
srv := NewServer(nil, nil, "http://localhost", false, false, false, false, false)
r.Get("/health", srv.HandleHealth)
ts := httptest.NewServer(r)
+5 -5
View File
@@ -13,7 +13,7 @@ import (
)
func TestHandleMgmtSpotifyInit(t *testing.T) {
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false)
// No spotify service configured
req := httptest.NewRequest("POST", "/mgmt/spotify/init", nil)
w := httptest.NewRecorder()
@@ -45,7 +45,7 @@ func TestHandleMgmtSpotifyInit(t *testing.T) {
}
func TestHandleMgmtSpotifyAccounts(t *testing.T) {
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false)
svc := spotify.NewSpotifyService("cid", "secret", "http://localhost/cb", t.TempDir())
s.SetSpotifyService(svc)
@@ -91,7 +91,7 @@ func TestHandleMgmtListSpeakers(t *testing.T) {
}
func TestHandleMgmtSpotifyCallback(t *testing.T) {
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false)
svc := spotify.NewSpotifyService("cid", "secret", "http://localhost/cb", t.TempDir())
s.SetSpotifyService(svc)
@@ -150,7 +150,7 @@ func TestHandleMgmtSpotifyCallback(t *testing.T) {
}
func TestHandleMgmtSpotifyConfirm(t *testing.T) {
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false)
svc := spotify.NewSpotifyService("cid", "secret", "http://localhost/cb", t.TempDir())
s.SetSpotifyService(svc)
@@ -191,7 +191,7 @@ func TestHandleMgmtDeviceEvents(t *testing.T) {
}
func TestBasicAuthMgmt(t *testing.T) {
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(nil, nil, "http://localhost", false, false, false, false, false)
s.SetMgmtConfig("admin", "secret123")
handler := s.BasicAuthMgmt()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-75
View File
@@ -98,84 +98,9 @@ func (s *Server) ServeProxy(target *url.URL) http.HandlerFunc {
// HandleNotFound handles requests that don't match any route.
func (s *Server) HandleNotFound(w http.ResponseWriter, r *http.Request) {
if s.enableSoundcorkProxy {
s.HandleSoundcorkWithFallback(w, r)
return
}
s.HandleBoseProxy(w, r)
}
// HandleSoundcorkWithFallback tries Soundcork first, then Bose if Soundcork returns 404 or fails.
func (s *Server) HandleSoundcorkWithFallback(w http.ResponseWriter, r *http.Request) {
target, _ := url.Parse(s.soundcorkURL)
// Buffer request body if any, to allow multiple proxy attempts
var bodyBytes []byte
if r.Body != nil {
bodyBytes, _ = io.ReadAll(r.Body)
_ = r.Body.Close()
}
// We use a custom response writer to catch 404s
rw := &fallbackResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
buffer: &bytes.Buffer{},
}
// Create a shallow copy of the request to avoid side effects between attempts
r2 := r.Clone(r.Context())
if bodyBytes != nil {
r2.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
} else {
r2.Body = nil
}
// Remove RequestURI as it's not allowed in client requests
r2.RequestURI = ""
s.ServeProxy(target)(rw, r2)
if rw.statusCode == http.StatusNotFound || rw.statusCode == http.StatusBadGateway || rw.statusCode == http.StatusServiceUnavailable {
log.Printf("[PROXY] Soundcork returned %d for %s, falling back to Bose", rw.statusCode, r.URL.Path)
if !rw.wroteHeader {
// Restore original body if any
if bodyBytes != nil {
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
s.HandleBoseProxy(w, r)
}
}
}
type fallbackResponseWriter struct {
http.ResponseWriter
statusCode int
wroteHeader bool
buffer *bytes.Buffer
}
func (rw *fallbackResponseWriter) WriteHeader(code int) {
rw.statusCode = code
if code != http.StatusNotFound && code != http.StatusBadGateway && code != http.StatusServiceUnavailable {
rw.wroteHeader = true
rw.ResponseWriter.WriteHeader(code)
}
}
func (rw *fallbackResponseWriter) Write(b []byte) (int, error) {
if rw.statusCode == http.StatusNotFound || rw.statusCode == http.StatusBadGateway || rw.statusCode == http.StatusServiceUnavailable {
return len(b), nil // Drop the body
}
rw.wroteHeader = true
return rw.ResponseWriter.Write(b)
}
// HandleBoseProxy proxies the request to the Bose upstream.
func (s *Server) HandleBoseProxy(w http.ResponseWriter, r *http.Request) {
host := r.Host
+1 -1
View File
@@ -33,7 +33,7 @@ func TestHandleProxyRequest_RequestBodyRecording(t *testing.T) {
defer backend.Close()
ds := datastore.NewDataStore(filepath.Join(tmpDir, "test.db"))
server := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
server := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
server.recordEnabled = true
server.proxyLogBody = true
recorder := proxy.NewRecorder(tmpDir)
+61 -75
View File
@@ -148,7 +148,7 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
s.mu.RLock()
serverURL, soundcorkURL, httpsServerURL := s.serverURL, s.soundcorkURL, s.httpsServerURL
serverURL, httpsServerURL := s.serverURL, s.httpsServerURL
discoveryInterval := s.discoveryInterval.String()
discoveryEnabled := s.discoveryEnabled
dnsEnabled := s.dnsEnabled
@@ -158,7 +158,6 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
mirrorEndpoints := s.mirrorEndpoints
preferredSource := s.preferredSource
internalPaths := s.internalPaths
enableSoundcorkProxy := s.enableSoundcorkProxy
redact, logBody, record := s.proxyRedact, s.proxyLogBody, s.recordEnabled
shortcuts := s.shortcuts
spotifyConfigured := s.spotifyService != nil
@@ -167,26 +166,24 @@ 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,
"soundcork_url": soundcorkURL,
"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,
"enable_soundcork_proxy": enableSoundcorkProxy,
"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,
"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
@@ -196,19 +193,17 @@ 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"`
SoundcorkURL string `json:"soundcork_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"`
EnableSoundcorkProxy bool `json:"enable_soundcork_proxy"`
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"`
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)
@@ -230,7 +225,6 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
s.serverURL = settings.ServerURL
s.soundcorkURL = settings.SoundcorkURL
if settings.DiscoveryInterval != "" {
s.discoveryInterval = interval
}
@@ -258,7 +252,6 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
s.preferredSource = settings.PreferredSource
s.internalPaths = settings.InternalPaths
s.enableSoundcorkProxy = settings.EnableSoundcorkProxy
if settings.Shortcuts != nil {
s.shortcuts = settings.Shortcuts
}
@@ -276,23 +269,21 @@ 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,
SoundcorkURL: s.soundcorkURL,
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,
EnableSoundcorkProxy: s.enableSoundcorkProxy,
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,
PreferredSource: s.preferredSource,
InternalPaths: s.internalPaths,
Shortcuts: s.shortcuts,
})
dnsEnabled := s.dnsEnabled
@@ -793,13 +784,12 @@ func (s *Server) HandleBackupConfig(w http.ResponseWriter, r *http.Request) {
func (s *Server) HandleGetProxySettings(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
redact, logBody, record, enableSoundcorkProxy := s.GetProxySettings()
redact, logBody, record := s.GetProxySettings()
if err := json.NewEncoder(w).Encode(map[string]interface{}{
"redact": redact,
"log_body": logBody,
"record": record,
"enable_soundcork_proxy": enableSoundcorkProxy,
"redact": redact,
"log_body": logBody,
"record": record,
}); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
@@ -824,10 +814,9 @@ func (s *Server) HandleGetCACert(w http.ResponseWriter, _ *http.Request) {
// HandleUpdateProxySettings updates the proxy settings.
func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Request) {
var settings struct {
Redact bool `json:"redact"`
LogBody bool `json:"log_body"`
Record bool `json:"record"`
EnableSoundcorkProxy bool `json:"enable_soundcork_proxy"`
Redact bool `json:"redact"`
LogBody bool `json:"log_body"`
Record bool `json:"record"`
}
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -838,7 +827,6 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques
s.proxyRedact = settings.Redact
s.proxyLogBody = settings.LogBody
s.recordEnabled = settings.Record
s.enableSoundcorkProxy = settings.EnableSoundcorkProxy
if s.recorder != nil {
s.recorder.Redact = settings.Redact
@@ -846,22 +834,20 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques
// Persist to datastore
// Access fields directly since we already hold the lock
serverURL, soundcorkURL, httpsServerURL := s.serverURL, s.soundcorkURL, s.httpsServerURL
serverURL, httpsServerURL := s.serverURL, s.httpsServerURL
discoveryInterval := s.discoveryInterval.String()
discoveryEnabled := s.discoveryEnabled
log.Printf("Saving updated proxy settings to %s/settings.json", s.ds.DataDir)
err := s.ds.SaveSettings(datastore.Settings{
ServerURL: serverURL,
SoundcorkURL: soundcorkURL,
HTTPServerURL: httpsServerURL,
RedactLogs: s.proxyRedact,
LogBodies: s.proxyLogBody,
RecordInteractions: s.recordEnabled,
DiscoveryInterval: discoveryInterval,
DiscoveryEnabled: discoveryEnabled,
EnableSoundcorkProxy: s.enableSoundcorkProxy,
Shortcuts: s.shortcuts,
ServerURL: serverURL,
HTTPServerURL: httpsServerURL,
RedactLogs: s.proxyRedact,
LogBodies: s.proxyLogBody,
RecordInteractions: s.recordEnabled,
DiscoveryInterval: discoveryInterval,
DiscoveryEnabled: discoveryEnabled,
Shortcuts: s.shortcuts,
})
s.mu.Unlock()
+4 -6
View File
@@ -101,8 +101,7 @@ func TestProxySettingsAPI(t *testing.T) {
// 3. Test System Settings POST
sysUpdate := map[string]string{
"server_url": "http://new-server:8000",
"soundcork_url": "http://new-proxy:8001",
"server_url": "http://new-server:8000",
}
sysBody, err := json.Marshal(sysUpdate)
@@ -122,15 +121,14 @@ func TestProxySettingsAPI(t *testing.T) {
}
// Verify server state
sURL, pURL, _ := server.GetSettings()
if sURL != "http://new-server:8000" || pURL != "http://new-proxy:8001" {
t.Errorf("POST /setup/settings: Server state did not update: serverURL=%s, soundcorkURL=%s", sURL, pURL)
sURL, _ := server.GetSettings()
if sURL != "http://new-server:8000" {
t.Errorf("POST /setup/settings: Server state did not update: serverURL=%s", sURL)
}
// 4. Test Mirror Settings persistence
mirrorUpdate := map[string]interface{}{
"server_url": "http://mirror-test:8000",
"soundcork_url": "http://mirror-test:8001",
"mirror_enabled": true,
"mirror_endpoints": []string{"/test/*"},
"internal_paths": []string{"/setup/*"},
+1 -1
View File
@@ -20,7 +20,7 @@ func TestStatsHandlers(t *testing.T) {
defer func() { _ = os.RemoveAll(tempDir) }()
ds := datastore.NewDataStore(tempDir)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
t.Run("HandleUsageStats XML", func(t *testing.T) {
xmlData := `
+2 -2
View File
@@ -23,7 +23,7 @@ func TestInteractionHandlers(t *testing.T) {
defer os.RemoveAll(tmpDir)
ds := datastore.NewDataStore(filepath.Join(tmpDir, "test.db"))
server := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
server := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
t.Run("HandleGetInteractionStats_NoRecorder", func(t *testing.T) {
req := httptest.NewRequest("GET", "/setup/interaction-stats", nil)
@@ -151,7 +151,7 @@ func TestRecordMiddleware(t *testing.T) {
defer os.RemoveAll(tmpDir)
ds := datastore.NewDataStore(filepath.Join(tmpDir, "test.db"))
server := NewServer(ds, nil, "http://localhost", false, false, true, false, false, false)
server := NewServer(ds, nil, "http://localhost", false, false, true, false, false)
recorder := proxy.NewRecorder(tmpDir)
server.SetRecorder(recorder)
@@ -74,7 +74,7 @@ func TestMACBasedDeviceDiscovery_Integration(t *testing.T) {
sm := setup.NewManager(server.URL, ds, nil)
// Create server instance
srv := NewServer(ds, sm, "http://localhost", false, false, false, false, false, false)
srv := NewServer(ds, sm, "http://localhost", false, false, false, false, false)
t.Logf("Test scenario:")
t.Logf(" Device IP: %s", deviceIP)
@@ -311,7 +311,7 @@ func TestMACBasedDeviceDiscovery_MigrationScenario(t *testing.T) {
deviceIP := server.URL[len("http://"):]
sm := setup.NewManager(server.URL, ds, nil)
srv := NewServer(ds, sm, server.URL, false, false, false, false, false, false)
srv := NewServer(ds, sm, server.URL, false, false, false, false, false)
// 3. Simulate rediscovery of the same device (now with /info working)
discoveredDevice := models.DiscoveredDevice{
@@ -380,7 +380,7 @@ func TestMACBasedDeviceDiscovery_FallbackScenario(t *testing.T) {
ds := datastore.NewDataStore(tempDir)
sm := setup.NewManager(server.URL, ds, nil)
srv := NewServer(ds, sm, server.URL, false, false, false, false, false, false)
srv := NewServer(ds, sm, server.URL, false, false, false, false, false)
// Simulate device discovery with UPnP providing serial
discoveredDevice := models.DiscoveredDevice{
@@ -97,7 +97,7 @@ func TestMacMappingIntegration_HTTPHandler(t *testing.T) {
t.Fatalf("failed to initialize datastore: %v", err)
}
server := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
server := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
// Setup router with the exact same route as in production
router := chi.NewRouter()
+1 -2
View File
@@ -6,8 +6,7 @@ import (
)
func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server) {
server := NewServer(ds, nil, targetURL, false, false, false, false, false, false)
server.SetSoundcorkURL(targetURL)
server := NewServer(ds, nil, targetURL, false, false, false, false, false)
r := chi.NewRouter()
r.Use(server.OriginMiddleware)
@@ -39,7 +39,7 @@ func TestMirrorMiddleware_PreferredSource(t *testing.T) {
defer upstreamServer.Close()
// 3. Setup our server with MirrorMiddleware
server := NewServer(ds, nil, "http://localhost:8000", false, false, false, false, false, false)
server := NewServer(ds, nil, "http://localhost:8000", false, false, false, false, false)
server.SetMirrorSettings(true, []string{"/test/local"}, "local")
// We need to trick performMirror to use our mock upstream.
@@ -119,7 +119,7 @@ func TestSettingsAPI_PreferredSource(t *testing.T) {
ds := datastore.NewDataStore(tempDir)
_ = ds.Initialize()
server := NewServer(ds, nil, "http://localhost:8000", false, false, false, false, false, false)
server := NewServer(ds, nil, "http://localhost:8000", false, false, false, false, false)
// Test GET initial
req := httptest.NewRequest("GET", "/setup/settings", nil)
+46 -58
View File
@@ -25,40 +25,38 @@ import (
// Server handles HTTP requests for the SoundTouch service.
type Server struct {
ds *datastore.DataStore
sm *setup.Manager
migrationManager *migration.Manager
mu sync.RWMutex
serverURL string
soundcorkURL string
httpsServerURL string
discovering bool
proxyRedact bool
proxyLogBody bool
recordEnabled bool
discoveryInterval time.Duration
discoveryEnabled bool
dnsEnabled bool
dnsUpstream []string
dnsBindAddr string
mirrorEnabled bool
mirrorEndpoints []string
preferredSource string
internalPaths []string
enableSoundcorkProxy bool
shortcuts map[string]int
recorder *proxy.Recorder
dnsDiscovery *discovery.DNSDiscovery
UpstreamProxy http.Handler
Version string
Commit string
Date string
mgmtUsername string
mgmtPassword string
spotifyClientID string
spotifyClientSecret string
spotifyRedirectURI string
spotifyService *spotify.Service
ds *datastore.DataStore
sm *setup.Manager
migrationManager *migration.Manager
mu sync.RWMutex
serverURL string
httpsServerURL string
discovering bool
proxyRedact bool
proxyLogBody bool
recordEnabled bool
discoveryInterval time.Duration
discoveryEnabled bool
dnsEnabled bool
dnsUpstream []string
dnsBindAddr string
mirrorEnabled bool
mirrorEndpoints []string
preferredSource string
internalPaths []string
shortcuts map[string]int
recorder *proxy.Recorder
dnsDiscovery *discovery.DNSDiscovery
UpstreamProxy http.Handler
Version string
Commit string
Date string
mgmtUsername string
mgmtPassword string
spotifyClientID string
spotifyClientSecret string
spotifyRedirectURI string
spotifyService *spotify.Service
}
// RequestSnapshot represents an immutable snapshot of an HTTP request.
@@ -83,7 +81,7 @@ var bufferPool = sync.Pool{
}
// NewServer creates a new SoundTouch service server.
func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled, enableSoundcorkProxy, migrationEnabled, migrationDryRun bool) *Server {
func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled, migrationEnabled, migrationDryRun bool) *Server {
// Initialize migration manager
migrationConfig := migration.Config{
Enabled: migrationEnabled,
@@ -91,17 +89,15 @@ func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, pro
}
s := &Server{
ds: ds,
sm: sm,
migrationManager: migration.NewManager(ds, migrationConfig),
serverURL: serverURL,
soundcorkURL: "http://localhost:8001",
proxyRedact: proxyRedact,
proxyLogBody: proxyLogBody,
recordEnabled: recordEnabled,
enableSoundcorkProxy: enableSoundcorkProxy,
discoveryInterval: 5 * time.Minute,
discoveryEnabled: true,
ds: ds,
sm: sm,
migrationManager: migration.NewManager(ds, migrationConfig),
serverURL: serverURL,
proxyRedact: proxyRedact,
proxyLogBody: proxyLogBody,
recordEnabled: recordEnabled,
discoveryInterval: 5 * time.Minute,
discoveryEnabled: true,
}
return s
@@ -301,14 +297,6 @@ func (s *Server) SetHTTPServerURL(url string) {
s.httpsServerURL = url
}
// SetSoundcorkURL sets the URL for the Soundcork backend.
func (s *Server) SetSoundcorkURL(url string) {
s.mu.Lock()
defer s.mu.Unlock()
s.soundcorkURL = url
}
// SetRecorder sets the recorder for the server.
func (s *Server) SetRecorder(r *proxy.Recorder) {
s.mu.Lock()
@@ -374,11 +362,11 @@ func (s *Server) GetRecordEnabled() bool {
}
// GetSettings returns the current server settings.
func (s *Server) GetSettings() (string, string, string) {
func (s *Server) GetSettings() (string, string) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.serverURL, s.soundcorkURL, s.httpsServerURL
return s.serverURL, s.httpsServerURL
}
// IsSpotifyConfigured returns whether Spotify integration is configured.
@@ -390,11 +378,11 @@ func (s *Server) IsSpotifyConfigured() bool {
}
// GetProxySettings returns the current proxy settings.
func (s *Server) GetProxySettings() (bool, bool, bool, bool) {
func (s *Server) GetProxySettings() (bool, bool, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.proxyRedact, s.proxyLogBody, s.recordEnabled, s.enableSoundcorkProxy
return s.proxyRedact, s.proxyLogBody, s.recordEnabled
}
// DiscoverDevices starts a background device discovery process.
+2 -2
View File
@@ -16,7 +16,7 @@ func TestMergeOverlappingDevices(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
// Case 1: IP-only entry and Serial-based entry for the same IP
ip := "192.168.1.100"
@@ -74,7 +74,7 @@ func TestFindExistingDeviceID(t *testing.T) {
defer os.RemoveAll(tempDir)
ds := datastore.NewDataStore(tempDir)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false, false)
s := NewServer(ds, nil, "http://localhost", false, false, false, false, false)
ip := "192.168.1.101"
serial := "SERIAL456"
@@ -25,7 +25,7 @@ func TestSnapshotIntegrity_SelfAndMirror(t *testing.T) {
ds := datastore.NewDataStore(tempDir)
recorder := proxy.NewRecorder(tempDir)
s := NewServer(ds, nil, "http://localhost:8000", false, false, true, false, false, false)
s := NewServer(ds, nil, "http://localhost:8000", false, false, true, false, false)
s.SetRecorder(recorder)
s.SetMirrorSettings(true, []string{"/mirror/*"}, "local")
-6
View File
@@ -87,11 +87,6 @@
<input type="text" id="target-domain" placeholder="http://192.168.x.x:8000" style="width: 300px;">
<span style="font-size: 0.8em; color: #666;">(Standard services URL)</span>
</div>
<div style="margin-bottom: 20px;">
<label for="soundcork-url">Soundcork URL:</label>
<input type="text" id="soundcork-url" placeholder="http://192.168.x.x:8001" style="width: 300px;">
<span style="font-size: 0.8em; color: #666;">(Soundcork services URL)</span>
</div>
<div style="margin-bottom: 20px;">
<label for="discovery-interval">Discovery Interval:</label>
<input type="text" id="discovery-interval" placeholder="5m" style="width: 100px;">
@@ -151,7 +146,6 @@
<div style="margin-top: 5px;">
<label style="display: block; margin-bottom: 5px;"><input type="checkbox" id="proxy-redact" onchange="updateProxySettings()"> Redact Sensitive Headers</label>
<label style="display: block; margin-bottom: 5px;"><input type="checkbox" id="proxy-log-body" onchange="updateProxySettings()"> Log Bodies</label>
<label style="display: block; margin-bottom: 5px;"><input type="checkbox" id="enable-soundcork-proxy" onchange="updateProxySettings()"> Enable Soundcork Proxy (Legacy)</label>
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" id="proxy-record" onchange="updateProxySettings()"> Record Interactions
<span style="font-size: 0.85em; color: #666; margin-left: 5px;">(View in <strong>5. Interactions</strong> tab)</span>
+2 -17
View File
@@ -126,9 +126,6 @@ async function fetchSettings() {
if (settings.server_url) {
document.getElementById('target-domain').value = settings.server_url;
}
if (settings.proxy_url) {
document.getElementById('soundcork-url').value = settings.proxy_url;
}
if (settings.discovery_interval) {
document.getElementById('discovery-interval').value = settings.discovery_interval;
}
@@ -165,10 +162,6 @@ async function fetchSettings() {
document.getElementById('internal-paths').value = settings.internal_paths.join('\n');
}
if (settings.enable_soundcork_proxy !== undefined) {
document.getElementById('enable-soundcork-proxy').checked = settings.enable_soundcork_proxy;
}
const spotifyStatus = document.getElementById('spotify-config-status');
if (spotifyStatus) {
if (settings.spotify_configured) {
@@ -193,9 +186,6 @@ async function fetchProxySettings() {
document.getElementById('proxy-redact').checked = settings.redact;
document.getElementById('proxy-log-body').checked = settings.log_body;
document.getElementById('proxy-record').checked = settings.record;
if (settings.enable_soundcork_proxy !== undefined) {
document.getElementById('enable-soundcork-proxy').checked = settings.enable_soundcork_proxy;
}
} catch (error) {
console.error('Failed to fetch proxy settings', error);
}
@@ -206,7 +196,6 @@ async function updateProxySettings() {
redact: document.getElementById('proxy-redact').checked,
log_body: document.getElementById('proxy-log-body').checked,
record: document.getElementById('proxy-record').checked,
enable_soundcork_proxy: document.getElementById('enable-soundcork-proxy').checked
};
try {
await fetch('/setup/proxy-settings', {
@@ -222,7 +211,6 @@ async function updateProxySettings() {
async function updateSettings() {
const settings = {
server_url: document.getElementById('target-domain').value,
proxy_url: document.getElementById('soundcork-url').value,
discovery_interval: document.getElementById('discovery-interval').value,
discovery_enabled: document.getElementById('discovery-enabled').checked,
dns_enabled: document.getElementById('dns-enabled').checked,
@@ -232,7 +220,6 @@ async function updateSettings() {
preferred_source: document.getElementById('preferred-source-upstream').checked ? 'upstream' : 'local',
mirror_endpoints: document.getElementById('mirror-endpoints').value.split('\n').map(s => s.trim()).filter(s => s !== ''),
internal_paths: document.getElementById('internal-paths').value.split('\n').map(s => s.trim()).filter(s => s !== ''),
enable_soundcork_proxy: document.getElementById('enable-soundcork-proxy').checked
};
const status = document.getElementById('settings-status');
status.innerText = 'Saving...';
@@ -1068,7 +1055,6 @@ async function showSummary(deviceId) {
return;
}
const targetUrl = document.getElementById('target-domain').value;
const proxyUrl = document.getElementById('soundcork-url').value;
const opts = {
marge: document.getElementById('opt-marge').value,
@@ -1087,7 +1073,7 @@ async function showSummary(deviceId) {
const outputBox = document.getElementById('command-output-box');
if (outputBox) outputBox.style.display = 'none';
let query = '?target_url=' + encodeURIComponent(targetUrl) + '&proxy_url=' + encodeURIComponent(proxyUrl);
let query = '?target_url=' + encodeURIComponent(targetUrl);
for (let k in opts) {
query += '&' + k + '=' + encodeURIComponent(opts[k]);
}
@@ -1334,7 +1320,6 @@ async function migrate(deviceId, ip) {
return;
}
const targetUrl = document.getElementById('target-domain').value;
const proxyUrl = document.getElementById('soundcork-url').value;
const method = document.getElementById('migration-method').value;
const opts = {
@@ -1353,7 +1338,7 @@ async function migrate(deviceId, ip) {
const display = getDeviceDisplayName(deviceId);
statusDiv.innerHTML = 'Migrating ' + display + ' using ' + method + '...';
let query = '?method=' + encodeURIComponent(method) + '&target_url=' + encodeURIComponent(targetUrl) + '&proxy_url=' + encodeURIComponent(proxyUrl);
let query = '?method=' + encodeURIComponent(method) + '&target_url=' + encodeURIComponent(targetUrl);
for (let k in opts) {
query += '&' + k + '=' + encodeURIComponent(opts[k]);
}