mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Optimize recording performance and add Soundcork proxy toggle
This commit introduces several key improvements: Performance Optimization (asynchronous recording), Legacy Proxy Control (Soundcork proxy toggle), X-Forwarded-For Sanitization, consistent Soundcork naming across the stack, and various code quality improvements.
This commit is contained in:
+135
-86
@@ -81,10 +81,15 @@ func main() {
|
||||
EnvVars: []string{"BIND_ADDR"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "target-url",
|
||||
Usage: "URL for Python-based service components (legacy)",
|
||||
Name: "soundcork-url",
|
||||
Usage: "URL for Soundcork-based service components (legacy)",
|
||||
Value: "http://localhost:8001",
|
||||
EnvVars: []string{"PYTHON_BACKEND_URL", "TARGET_URL"},
|
||||
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",
|
||||
@@ -138,47 +143,11 @@ func main() {
|
||||
config := loadConfig(c)
|
||||
ds := initDataStore(config.dataDir)
|
||||
|
||||
// Load settings from datastore
|
||||
persisted, err := ds.GetSettings()
|
||||
persisted := applyPersistedSettings(ds, &config)
|
||||
|
||||
settingsExist := err == nil && persisted.ServerURL != ""
|
||||
if persisted.ServerURL != "" {
|
||||
config.serverURL = persisted.ServerURL
|
||||
}
|
||||
|
||||
if persisted.ProxyURL != "" {
|
||||
config.targetURL = persisted.ProxyURL
|
||||
}
|
||||
|
||||
if persisted.HTTPServerURL != "" {
|
||||
config.httpsServerURL = persisted.HTTPServerURL
|
||||
}
|
||||
|
||||
if persisted.DiscoveryInterval != "" {
|
||||
if d, durErr := time.ParseDuration(persisted.DiscoveryInterval); durErr == nil {
|
||||
config.discoveryInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
config.redact = persisted.RedactLogs || config.redact
|
||||
config.logBody = persisted.LogBodies || config.logBody
|
||||
config.record = persisted.RecordInteractions || config.record
|
||||
|
||||
if !settingsExist {
|
||||
if persisted.ServerURL == "" {
|
||||
log.Printf("Creating default settings.json in %s", config.dataDir)
|
||||
persisted.ServerURL = config.serverURL
|
||||
persisted.ProxyURL = config.targetURL
|
||||
persisted.HTTPServerURL = config.httpsServerURL
|
||||
persisted.RedactLogs = config.redact
|
||||
persisted.LogBodies = config.logBody
|
||||
persisted.RecordInteractions = config.record
|
||||
persisted.DiscoveryInterval = config.discoveryInterval.String()
|
||||
persisted.DiscoveryEnabled = true
|
||||
persisted.Shortcuts = map[string]int{
|
||||
"/.well-known/appspecific/com.chrome.devtools.json": http.StatusNotFound,
|
||||
"/sw.js": http.StatusNotFound,
|
||||
}
|
||||
_ = ds.SaveSettings(persisted)
|
||||
persisted = createDefaultSettings(ds, config)
|
||||
}
|
||||
|
||||
// Recalculate domains if settings changed
|
||||
@@ -191,7 +160,7 @@ func main() {
|
||||
|
||||
cm := initCertificateManager(config.dataDir)
|
||||
sm := setup.NewManager(config.serverURL, ds, cm)
|
||||
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record)
|
||||
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record, config.enableSoundcorkProxy)
|
||||
server.SetHTTPServerURL(config.httpsServerURL)
|
||||
server.SetVersionInfo(version, commit, date)
|
||||
server.SetDiscoverySettings(config.discoveryInterval, persisted.DiscoveryEnabled)
|
||||
@@ -234,13 +203,13 @@ func main() {
|
||||
log.Printf("Warning: Failed to setup TLS: %v", err)
|
||||
}
|
||||
|
||||
pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody, recorder, server)
|
||||
scProxy := setupSoundcorkProxy(config.soundcorkURL, config.redact, config.logBody, recorder, server)
|
||||
|
||||
startDeviceDiscovery(server)
|
||||
|
||||
r := setupRouter(server, pyProxy)
|
||||
r := setupRouter(server, scProxy, config.enableSoundcorkProxy)
|
||||
|
||||
log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.targetURL)
|
||||
log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.soundcorkURL)
|
||||
|
||||
if tlsConfig != nil {
|
||||
startHTTPSServer(config.httpsAddr, r, tlsConfig, config.httpsServerURL)
|
||||
@@ -274,19 +243,20 @@ func showVersionInfo(_ *cli.Context) error {
|
||||
}
|
||||
|
||||
type serviceConfig struct {
|
||||
port string
|
||||
bindAddr string
|
||||
addr string
|
||||
targetURL string
|
||||
dataDir string
|
||||
serverURL string
|
||||
httpsServerURL string
|
||||
httpsAddr string
|
||||
redact bool
|
||||
logBody bool
|
||||
record bool
|
||||
discoveryInterval time.Duration
|
||||
domains []string
|
||||
port string
|
||||
bindAddr string
|
||||
addr string
|
||||
soundcorkURL string
|
||||
dataDir string
|
||||
serverURL string
|
||||
httpsServerURL string
|
||||
httpsAddr string
|
||||
redact bool
|
||||
logBody bool
|
||||
record bool
|
||||
enableSoundcorkProxy bool
|
||||
discoveryInterval time.Duration
|
||||
domains []string
|
||||
}
|
||||
|
||||
func loadConfig(c *cli.Context) serviceConfig {
|
||||
@@ -298,7 +268,7 @@ func loadConfig(c *cli.Context) serviceConfig {
|
||||
addr = ":" + port
|
||||
}
|
||||
|
||||
targetURL := c.String("target-url")
|
||||
soundcorkURL := c.String("soundcork-url")
|
||||
dataDir := c.String("data-dir")
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
@@ -330,6 +300,7 @@ 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")
|
||||
|
||||
discoveryIntervalStr := c.String("discovery-interval")
|
||||
|
||||
@@ -341,19 +312,20 @@ func loadConfig(c *cli.Context) serviceConfig {
|
||||
}
|
||||
|
||||
return serviceConfig{
|
||||
port: port,
|
||||
bindAddr: bindAddr,
|
||||
addr: addr,
|
||||
targetURL: targetURL,
|
||||
dataDir: dataDir,
|
||||
serverURL: serverURL,
|
||||
httpsServerURL: httpsServerURL,
|
||||
httpsAddr: httpsAddr,
|
||||
redact: redact,
|
||||
logBody: logBody,
|
||||
record: record,
|
||||
discoveryInterval: discoveryInterval,
|
||||
domains: domains,
|
||||
port: port,
|
||||
bindAddr: bindAddr,
|
||||
addr: addr,
|
||||
soundcorkURL: soundcorkURL,
|
||||
dataDir: dataDir,
|
||||
serverURL: serverURL,
|
||||
httpsServerURL: httpsServerURL,
|
||||
httpsAddr: httpsAddr,
|
||||
redact: redact,
|
||||
logBody: logBody,
|
||||
record: record,
|
||||
enableSoundcorkProxy: enableSoundcorkProxy,
|
||||
discoveryInterval: discoveryInterval,
|
||||
domains: domains,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +358,59 @@ func getDomains(serverURL, httpsServerURL, hostname string) []string {
|
||||
return domains
|
||||
}
|
||||
|
||||
func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) datastore.Settings {
|
||||
persisted, err := ds.GetSettings()
|
||||
if err != nil {
|
||||
return datastore.Settings{}
|
||||
}
|
||||
|
||||
if persisted.ServerURL != "" {
|
||||
config.serverURL = persisted.ServerURL
|
||||
}
|
||||
|
||||
if persisted.SoundcorkURL != "" {
|
||||
config.soundcorkURL = persisted.SoundcorkURL
|
||||
}
|
||||
|
||||
if persisted.HTTPServerURL != "" {
|
||||
config.httpsServerURL = persisted.HTTPServerURL
|
||||
}
|
||||
|
||||
if persisted.DiscoveryInterval != "" {
|
||||
if d, durErr := time.ParseDuration(persisted.DiscoveryInterval); durErr == nil {
|
||||
config.discoveryInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
config.redact = persisted.RedactLogs || config.redact
|
||||
config.logBody = persisted.LogBodies || config.logBody
|
||||
config.record = persisted.RecordInteractions || config.record
|
||||
config.enableSoundcorkProxy = persisted.EnableSoundcorkProxy || config.enableSoundcorkProxy
|
||||
|
||||
return persisted
|
||||
}
|
||||
|
||||
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,
|
||||
Shortcuts: map[string]int{
|
||||
"/.well-known/appspecific/com.chrome.devtools.json": http.StatusNotFound,
|
||||
"/sw.js": http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
_ = ds.SaveSettings(settings)
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
func initDataStore(dataDir string) *datastore.DataStore {
|
||||
ds := datastore.NewDataStore(dataDir)
|
||||
if err := ds.Initialize(); err != nil {
|
||||
@@ -404,14 +429,14 @@ func initCertificateManager(dataDir string) *certmanager.CertificateManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Recorder, server *handlers.Server) *httputil.ReverseProxy {
|
||||
target, err := url.Parse(targetURL)
|
||||
func setupSoundcorkProxy(soundcorkURL string, redact, logBody bool, recorder *proxy.Recorder, server *handlers.Server) *httputil.ReverseProxy {
|
||||
target, err := url.Parse(soundcorkURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse target URL: %v", err)
|
||||
log.Fatalf("Failed to parse Soundcork URL: %v", err)
|
||||
}
|
||||
|
||||
pyProxy := httputil.NewSingleHostReverseProxy(target)
|
||||
pyProxy.ModifyResponse = func(res *http.Response) error {
|
||||
scProxy := httputil.NewSingleHostReverseProxy(target)
|
||||
scProxy.ModifyResponse = func(res *http.Response) error {
|
||||
if etags, ok := res.Header["Etag"]; ok {
|
||||
delete(res.Header, "Etag")
|
||||
res.Header["ETag"] = etags
|
||||
@@ -426,9 +451,31 @@ func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Re
|
||||
return nil
|
||||
}
|
||||
|
||||
originalPyDirector := pyProxy.Director
|
||||
pyProxy.Director = func(req *http.Request) {
|
||||
originalPyDirector(req)
|
||||
originalScDirector := scProxy.Director
|
||||
scProxy.Director = func(req *http.Request) {
|
||||
originalScDirector(req)
|
||||
|
||||
// Fix X-Forwarded-For bloat by deduplicating
|
||||
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
parts := strings.Split(xff, ",")
|
||||
seen := make(map[string]bool)
|
||||
unique := make([]string, 0, len(parts))
|
||||
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" && !seen[p] {
|
||||
seen[p] = true
|
||||
unique = append(unique, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the number of entries to prevent header overflow
|
||||
if len(unique) > 10 {
|
||||
unique = unique[len(unique)-10:]
|
||||
}
|
||||
|
||||
req.Header.Set("X-Forwarded-For", strings.Join(unique, ", "))
|
||||
}
|
||||
|
||||
currentLp := proxy.NewLoggingProxy(target.String(), redact)
|
||||
currentLp.LogBody = logBody
|
||||
@@ -437,7 +484,7 @@ func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Re
|
||||
currentLp.LogRequest(req)
|
||||
}
|
||||
|
||||
return pyProxy
|
||||
return scProxy
|
||||
}
|
||||
|
||||
func startDeviceDiscovery(server *handlers.Server) {
|
||||
@@ -453,7 +500,7 @@ func startDeviceDiscovery(server *handlers.Server) {
|
||||
}()
|
||||
}
|
||||
|
||||
func setupRouter(server *handlers.Server, pyProxy *httputil.ReverseProxy) *chi.Mux {
|
||||
func setupRouter(server *handlers.Server, scProxy *httputil.ReverseProxy, enableSoundcorkProxy bool) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
@@ -547,9 +594,11 @@ func setupRouter(server *handlers.Server, pyProxy *httputil.ReverseProxy) *chi.M
|
||||
r.Get("/devices/{deviceId}/events", server.HandleGetDeviceEvents)
|
||||
})
|
||||
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
pyProxy.ServeHTTP(w, r)
|
||||
})
|
||||
if enableSoundcorkProxy {
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
scProxy.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -698,15 +698,16 @@ func (ds *DataStore) GetETagForAccount(account, device string) int64 {
|
||||
|
||||
// Settings represents the global service settings.
|
||||
type Settings struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
ProxyURL string `json:"proxy_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"`
|
||||
Shortcuts map[string]int `json:"shortcuts,omitempty"`
|
||||
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"`
|
||||
Shortcuts map[string]int `json:"shortcuts,omitempty"`
|
||||
}
|
||||
|
||||
// GetSettings retrieves the global service settings.
|
||||
|
||||
@@ -382,7 +382,7 @@ func TestSettingsPersistence(t *testing.T) {
|
||||
|
||||
settings := Settings{
|
||||
ServerURL: "http://myserver:8000",
|
||||
ProxyURL: "http://myproxy:8001",
|
||||
SoundcorkURL: "http://myproxy:8001",
|
||||
LogBodies: true,
|
||||
DiscoveryInterval: "10m",
|
||||
DiscoveryEnabled: true,
|
||||
|
||||
@@ -143,17 +143,25 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
s.mu.RLock()
|
||||
serverURL, proxyURL, httpsServerURL := s.serverURL, s.proxyURL, s.httpsServerURL
|
||||
serverURL, soundcorkURL, httpsServerURL := s.serverURL, s.soundcorkURL, s.httpsServerURL
|
||||
discoveryInterval := s.discoveryInterval.String()
|
||||
discoveryEnabled := s.discoveryEnabled
|
||||
enableSoundcorkProxy := s.enableSoundcorkProxy
|
||||
redact, logBody, record := s.proxyRedact, s.proxyLogBody, s.recordEnabled
|
||||
shortcuts := s.shortcuts
|
||||
s.mu.RUnlock()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"server_url": serverURL,
|
||||
"proxy_url": proxyURL,
|
||||
"https_server_url": httpsServerURL,
|
||||
"discovery_interval": discoveryInterval,
|
||||
"discovery_enabled": discoveryEnabled,
|
||||
"server_url": serverURL,
|
||||
"soundcork_url": soundcorkURL,
|
||||
"https_server_url": httpsServerURL,
|
||||
"discovery_interval": discoveryInterval,
|
||||
"discovery_enabled": discoveryEnabled,
|
||||
"enable_soundcork_proxy": enableSoundcorkProxy,
|
||||
"redact_logs": redact,
|
||||
"log_bodies": logBody,
|
||||
"record_interactions": record,
|
||||
"shortcuts": shortcuts,
|
||||
}); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -163,10 +171,12 @@ 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"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
DiscoveryInterval string `json:"discovery_interval"`
|
||||
DiscoveryEnabled bool `json:"discovery_enabled"`
|
||||
ServerURL string `json:"server_url"`
|
||||
SoundcorkURL string `json:"soundcork_url"`
|
||||
DiscoveryInterval string `json:"discovery_interval"`
|
||||
DiscoveryEnabled bool `json:"discovery_enabled"`
|
||||
EnableSoundcorkProxy bool `json:"enable_soundcork_proxy"`
|
||||
Shortcuts map[string]int `json:"shortcuts"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -182,13 +192,18 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
s.mu.Lock()
|
||||
s.serverURL = settings.ServerURL
|
||||
|
||||
s.proxyURL = settings.ProxyURL
|
||||
s.soundcorkURL = settings.SoundcorkURL
|
||||
if settings.DiscoveryInterval != "" {
|
||||
s.discoveryInterval = interval
|
||||
}
|
||||
|
||||
s.discoveryEnabled = settings.DiscoveryEnabled
|
||||
|
||||
s.enableSoundcorkProxy = settings.EnableSoundcorkProxy
|
||||
if settings.Shortcuts != nil {
|
||||
s.shortcuts = settings.Shortcuts
|
||||
}
|
||||
|
||||
if s.sm != nil {
|
||||
s.sm.ServerURL = settings.ServerURL
|
||||
}
|
||||
@@ -202,14 +217,16 @@ 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,
|
||||
ProxyURL: s.proxyURL,
|
||||
HTTPServerURL: currentHTTPS,
|
||||
RedactLogs: currentRedact,
|
||||
LogBodies: currentLogBody,
|
||||
RecordInteractions: currentRecord,
|
||||
DiscoveryInterval: s.discoveryInterval.String(),
|
||||
DiscoveryEnabled: s.discoveryEnabled,
|
||||
ServerURL: s.serverURL,
|
||||
SoundcorkURL: s.soundcorkURL,
|
||||
HTTPServerURL: currentHTTPS,
|
||||
RedactLogs: currentRedact,
|
||||
LogBodies: currentLogBody,
|
||||
RecordInteractions: currentRecord,
|
||||
DiscoveryInterval: s.discoveryInterval.String(),
|
||||
DiscoveryEnabled: s.discoveryEnabled,
|
||||
EnableSoundcorkProxy: s.enableSoundcorkProxy,
|
||||
Shortcuts: s.shortcuts,
|
||||
})
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -513,12 +530,13 @@ 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 := s.GetProxySettings()
|
||||
redact, logBody, record, enableSoundcorkProxy := s.GetProxySettings()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]bool{
|
||||
"redact": redact,
|
||||
"log_body": logBody,
|
||||
"record": record,
|
||||
if err := json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"redact": redact,
|
||||
"log_body": logBody,
|
||||
"record": record,
|
||||
"enable_soundcork_proxy": enableSoundcorkProxy,
|
||||
}); err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -543,9 +561,10 @@ 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"`
|
||||
Redact bool `json:"redact"`
|
||||
LogBody bool `json:"log_body"`
|
||||
Record bool `json:"record"`
|
||||
EnableSoundcorkProxy bool `json:"enable_soundcork_proxy"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -556,23 +575,26 @@ 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
|
||||
|
||||
// Persist to datastore
|
||||
// Access fields directly since we already hold the lock
|
||||
serverURL, proxyURL, httpsServerURL := s.serverURL, s.proxyURL, s.httpsServerURL
|
||||
serverURL, soundcorkURL, httpsServerURL := s.serverURL, s.soundcorkURL, 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,
|
||||
ProxyURL: proxyURL,
|
||||
HTTPServerURL: httpsServerURL,
|
||||
RedactLogs: s.proxyRedact,
|
||||
LogBodies: s.proxyLogBody,
|
||||
RecordInteractions: s.recordEnabled,
|
||||
DiscoveryInterval: discoveryInterval,
|
||||
DiscoveryEnabled: discoveryEnabled,
|
||||
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,
|
||||
})
|
||||
s.mu.Unlock()
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ func TestProxySettingsAPI(t *testing.T) {
|
||||
|
||||
// 3. Test System Settings POST
|
||||
sysUpdate := map[string]string{
|
||||
"server_url": "http://new-server:8000",
|
||||
"proxy_url": "http://new-proxy:8001",
|
||||
"server_url": "http://new-server:8000",
|
||||
"soundcork_url": "http://new-proxy:8001",
|
||||
}
|
||||
|
||||
sysBody, err := json.Marshal(sysUpdate)
|
||||
@@ -121,7 +121,7 @@ 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, proxyURL=%s", sURL, pURL)
|
||||
t.Errorf("POST /setup/settings: Server state did not update: serverURL=%s, soundcorkURL=%s", sURL, pURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,36 +15,38 @@ import (
|
||||
|
||||
// Server handles HTTP requests for the SoundTouch service.
|
||||
type Server struct {
|
||||
ds *datastore.DataStore
|
||||
sm *setup.Manager
|
||||
mu sync.RWMutex
|
||||
serverURL string
|
||||
proxyURL string
|
||||
httpsServerURL string
|
||||
discovering bool
|
||||
proxyRedact bool
|
||||
proxyLogBody bool
|
||||
recordEnabled bool
|
||||
discoveryInterval time.Duration
|
||||
discoveryEnabled bool
|
||||
shortcuts map[string]int
|
||||
recorder *proxy.Recorder
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
ds *datastore.DataStore
|
||||
sm *setup.Manager
|
||||
mu sync.RWMutex
|
||||
serverURL string
|
||||
soundcorkURL string
|
||||
httpsServerURL string
|
||||
discovering bool
|
||||
proxyRedact bool
|
||||
proxyLogBody bool
|
||||
recordEnabled bool
|
||||
discoveryInterval time.Duration
|
||||
discoveryEnabled bool
|
||||
enableSoundcorkProxy bool
|
||||
shortcuts map[string]int
|
||||
recorder *proxy.Recorder
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
}
|
||||
|
||||
// NewServer creates a new SoundTouch service server.
|
||||
func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled bool) *Server {
|
||||
func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled, enableSoundcorkProxy bool) *Server {
|
||||
return &Server{
|
||||
ds: ds,
|
||||
sm: sm,
|
||||
serverURL: serverURL,
|
||||
proxyURL: serverURL,
|
||||
proxyRedact: proxyRedact,
|
||||
proxyLogBody: proxyLogBody,
|
||||
recordEnabled: recordEnabled,
|
||||
discoveryInterval: 5 * time.Minute,
|
||||
ds: ds,
|
||||
sm: sm,
|
||||
serverURL: serverURL,
|
||||
soundcorkURL: serverURL,
|
||||
proxyRedact: proxyRedact,
|
||||
proxyLogBody: proxyLogBody,
|
||||
recordEnabled: recordEnabled,
|
||||
enableSoundcorkProxy: enableSoundcorkProxy,
|
||||
discoveryInterval: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,15 +119,15 @@ func (s *Server) GetSettings() (string, string, string) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.serverURL, s.proxyURL, s.httpsServerURL
|
||||
return s.serverURL, s.soundcorkURL, s.httpsServerURL
|
||||
}
|
||||
|
||||
// GetProxySettings returns the current proxy settings.
|
||||
func (s *Server) GetProxySettings() (bool, bool, bool) {
|
||||
func (s *Server) GetProxySettings() (bool, bool, bool, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.proxyRedact, s.proxyLogBody, s.recordEnabled
|
||||
return s.proxyRedact, s.proxyLogBody, s.recordEnabled, s.enableSoundcorkProxy
|
||||
}
|
||||
|
||||
// DiscoverDevices starts a background device discovery process.
|
||||
|
||||
@@ -87,9 +87,9 @@
|
||||
<span style="font-size: 0.8em; color: #666;">(Standard services URL)</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="proxy-domain">Proxy Domain:</label>
|
||||
<input type="text" id="proxy-domain" placeholder="http://192.168.x.x:8000" style="width: 300px;">
|
||||
<span style="font-size: 0.8em; color: #666;">(Upstream proxy URL - usually the same as Target Domain)</span>
|
||||
<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>
|
||||
@@ -105,6 +105,7 @@
|
||||
<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>
|
||||
|
||||
@@ -6,7 +6,7 @@ async function fetchSettings() {
|
||||
document.getElementById('target-domain').value = settings.server_url;
|
||||
}
|
||||
if (settings.proxy_url) {
|
||||
document.getElementById('proxy-domain').value = settings.proxy_url;
|
||||
document.getElementById('soundcork-url').value = settings.proxy_url;
|
||||
}
|
||||
if (settings.discovery_interval) {
|
||||
document.getElementById('discovery-interval').value = settings.discovery_interval;
|
||||
@@ -14,6 +14,9 @@ async function fetchSettings() {
|
||||
if (settings.discovery_enabled !== undefined) {
|
||||
document.getElementById('discovery-enabled').checked = settings.discovery_enabled;
|
||||
}
|
||||
if (settings.enable_soundcork_proxy !== undefined) {
|
||||
document.getElementById('enable-soundcork-proxy').checked = settings.enable_soundcork_proxy;
|
||||
}
|
||||
fetchProxySettings();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings', error);
|
||||
@@ -27,6 +30,9 @@ 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);
|
||||
}
|
||||
@@ -36,7 +42,8 @@ async function updateProxySettings() {
|
||||
const settings = {
|
||||
redact: document.getElementById('proxy-redact').checked,
|
||||
log_body: document.getElementById('proxy-log-body').checked,
|
||||
record: document.getElementById('proxy-record').checked
|
||||
record: document.getElementById('proxy-record').checked,
|
||||
enable_soundcork_proxy: document.getElementById('enable-soundcork-proxy').checked
|
||||
};
|
||||
try {
|
||||
await fetch('/setup/proxy-settings', {
|
||||
@@ -52,9 +59,10 @@ async function updateProxySettings() {
|
||||
async function updateSettings() {
|
||||
const settings = {
|
||||
server_url: document.getElementById('target-domain').value,
|
||||
proxy_url: document.getElementById('proxy-domain').value,
|
||||
proxy_url: document.getElementById('soundcork-url').value,
|
||||
discovery_interval: document.getElementById('discovery-interval').value,
|
||||
discovery_enabled: document.getElementById('discovery-enabled').checked
|
||||
discovery_enabled: document.getElementById('discovery-enabled').checked,
|
||||
enable_soundcork_proxy: document.getElementById('enable-soundcork-proxy').checked
|
||||
};
|
||||
const status = document.getElementById('settings-status');
|
||||
status.innerText = 'Saving...';
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -25,6 +26,16 @@ type Recorder struct {
|
||||
counter uint64
|
||||
variables map[string]string
|
||||
mu sync.Mutex
|
||||
queue chan recordingTask
|
||||
}
|
||||
|
||||
type recordingTask struct {
|
||||
category string
|
||||
req *http.Request
|
||||
res *http.Response
|
||||
replacements map[string]string
|
||||
dir string
|
||||
path string
|
||||
}
|
||||
|
||||
// InteractionStats represents statistics for recorded interactions.
|
||||
@@ -51,15 +62,24 @@ type Interaction struct {
|
||||
func NewRecorder(baseDir string) *Recorder {
|
||||
sessionID := time.Now().Format("20060102-150405") + "-" + fmt.Sprintf("%d", os.Getpid())
|
||||
|
||||
return &Recorder{
|
||||
r := &Recorder{
|
||||
BaseDir: baseDir,
|
||||
SessionID: sessionID,
|
||||
Patterns: DefaultPatterns(),
|
||||
variables: make(map[string]string),
|
||||
}
|
||||
|
||||
// Use environment variable to control async recording, default to true for production
|
||||
// but allow disabling it for tests if needed.
|
||||
if os.Getenv("RECORDER_ASYNC") != "false" {
|
||||
r.queue = make(chan recordingTask, 100)
|
||||
go r.worker()
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Record persists a request and response to a .http file in the specified category (e.g., "self" or "upstream").
|
||||
// Record logs an interaction to the configured category.
|
||||
func (r *Recorder) Record(category string, req *http.Request, res *http.Response) error {
|
||||
if r.BaseDir == "" {
|
||||
return nil
|
||||
@@ -74,19 +94,51 @@ func (r *Recorder) Record(category string, req *http.Request, res *http.Response
|
||||
|
||||
path := r.getRecordingPath(dir, req.Method)
|
||||
|
||||
// Shallow copy request for the worker to avoid data races if the original is reused
|
||||
// but Note: body is already buffered/replaced in middleware if needed.
|
||||
// We need to be careful about bodies being closed.
|
||||
task := recordingTask{
|
||||
category: category,
|
||||
req: req,
|
||||
res: res,
|
||||
replacements: replacements,
|
||||
dir: dir,
|
||||
path: path,
|
||||
}
|
||||
|
||||
// For testing purposes or if queue is nil, fallback to synchronous
|
||||
if r.queue == nil {
|
||||
r.save(task)
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case r.queue <- task:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("recording queue full, dropping interaction for %s", req.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) save(task recordingTask) {
|
||||
var buf bytes.Buffer
|
||||
r.writeRequest(&buf, task.req, task.replacements)
|
||||
|
||||
r.writeRequest(&buf, req, replacements)
|
||||
|
||||
if res != nil {
|
||||
r.writeResponse(&buf, res)
|
||||
if task.res != nil {
|
||||
r.writeResponse(&buf, task.res)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
if err := os.WriteFile(task.path, buf.Bytes(), 0644); err != nil {
|
||||
log.Printf("failed to write recording to %s: %v", task.path, err)
|
||||
}
|
||||
|
||||
return r.updateEnvFile(replacements)
|
||||
_ = r.updateEnvFile(task.replacements)
|
||||
}
|
||||
|
||||
func (r *Recorder) worker() {
|
||||
for task := range r.queue {
|
||||
r.save(task)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Recorder) getSanitizedSegments(path string) ([]string, map[string]string) {
|
||||
|
||||
Reference in New Issue
Block a user