From 9be1c7d588805275ed2b648b26ee7e114c8d8f2b Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sat, 14 Feb 2026 12:33:16 +0100 Subject: [PATCH] Allow toggling HTTP interaction recording via CLI, environment, and Web UI --- cmd/soundtouch-service/main.go | 17 ++++++++-- docs/SOUNDTOUCH-SERVICE.md | 23 +++++++------ pkg/service/handlers/handlers_proxy.go | 1 + pkg/service/handlers/handlers_setup.go | 3 ++ pkg/service/handlers/recorder_middleware.go | 2 +- pkg/service/handlers/server.go | 37 ++++++++++++--------- pkg/service/handlers/web/index.html | 1 + pkg/service/handlers/web/js/script.js | 18 +++++----- pkg/service/proxy/proxy.go | 13 ++++---- 9 files changed, 71 insertions(+), 44 deletions(-) diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index bf33701..b8f7717 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -30,7 +30,7 @@ func main() { ds := initDataStore(config.dataDir) cm := initCertificateManager(config.dataDir) sm := setup.NewManager(config.serverURL, ds, cm) - server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody) + server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record) recorder := proxy.NewRecorder(config.dataDir) recorder.Redact = config.redact @@ -48,7 +48,7 @@ func main() { log.Printf("Warning: Failed to setup TLS: %v", err) } - pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody, recorder) + pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody, recorder, server) startDeviceDiscovery(server) @@ -74,6 +74,7 @@ type serviceConfig struct { httpsAddr string redact bool logBody bool + record bool domains []string } @@ -88,6 +89,7 @@ func loadConfig() serviceConfig { fHttpsServerURL := flag.String("https-server-url", "", "External HTTPS URL (env: HTTPS_SERVER_URL)") fRedact := flag.String("redact-logs", "", "Redact sensitive data in proxy logs (true/false, env: REDACT_PROXY_LOGS)") fLogBody := flag.String("log-bodies", "", "Log full request/response bodies (true/false, env: LOG_PROXY_BODY)") + fRecord := flag.String("record-interactions", "", "Record HTTP interactions to disk (true/false, env: RECORD_INTERACTIONS)") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of soundtouch-service:\n") @@ -214,6 +216,12 @@ func loadConfig() serviceConfig { } logBody := logBodyVal == "true" + recordVal := *fRecord + if recordVal == "" { + recordVal = os.Getenv("RECORD_INTERACTIONS") + } + record := recordVal != "false" + return serviceConfig{ port: port, bindAddr: bindAddr, @@ -225,6 +233,7 @@ func loadConfig() serviceConfig { httpsAddr: httpsAddr, redact: redact, logBody: logBody, + record: record, domains: domains, } } @@ -247,7 +256,7 @@ func initCertificateManager(dataDir string) *certmanager.CertificateManager { return cm } -func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Recorder) *httputil.ReverseProxy { +func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Recorder, server *handlers.Server) *httputil.ReverseProxy { target, err := url.Parse(targetURL) if err != nil { log.Fatalf("Failed to parse target URL: %v", err) @@ -262,6 +271,7 @@ func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Re currentLp := proxy.NewLoggingProxy(target.String(), redact) currentLp.LogBody = logBody + currentLp.RecordEnabled = server.GetRecordEnabled() currentLp.SetRecorder(recorder) currentLp.LogResponse(res) @@ -274,6 +284,7 @@ func setupPythonProxy(targetURL string, redact, logBody bool, recorder *proxy.Re currentLp := proxy.NewLoggingProxy(target.String(), redact) currentLp.LogBody = logBody + currentLp.RecordEnabled = server.GetRecordEnabled() currentLp.SetRecorder(recorder) currentLp.LogRequest(req) } diff --git a/docs/SOUNDTOUCH-SERVICE.md b/docs/SOUNDTOUCH-SERVICE.md index 2c6ca66..10a1515 100644 --- a/docs/SOUNDTOUCH-SERVICE.md +++ b/docs/SOUNDTOUCH-SERVICE.md @@ -135,17 +135,18 @@ Use the web interface or API to migrate devices from Bose cloud services to your The service can be configured via environment variables or command-line flags: -| Variable | Flag | Description | Default | -|----------------------|------------------------|--------------------------------------------------|---------------------------| -| `PORT` | `--port` | Port to bind the service to | `8000` | -| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) | -| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` | -| `SERVER_URL` | `--server-url` | External URL of this service | `http://:8000` | -| `HTTPS_SERVER_URL` | `--https-server-url` | External HTTPS URL | `https://:8443` | -| `PYTHON_BACKEND_URL` | | URL for Python-based service components (legacy) | | -| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` | -| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` | -| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` | +| Variable | Flag | Description | Default | +|-----------------------|-------------------------|--------------------------------------------------|---------------------------| +| `PORT` | `--port` | Port to bind the service to | `8000` | +| `BIND_ADDR` | `--bind` | Network interface to bind to | all (ipv4 and ipv6) | +| `DATA_DIR` | `--data-dir` | Directory for persistent data | `./data` | +| `SERVER_URL` | `--server-url` | External URL of this service | `http://:8000` | +| `HTTPS_SERVER_URL` | `--https-server-url` | External HTTPS URL | `https://:8443` | +| `PYTHON_BACKEND_URL` | | URL for Python-based service components (legacy) | | +| `REDACT_PROXY_LOGS` | `--redact-logs` | Redact sensitive data in proxy logs | `true` | +| `LOG_PROXY_BODY` | `--log-bodies` | Log full request/response bodies | `false` | +| `RECORD_INTERACTIONS` | `--record-interactions` | Record HTTP interactions to disk | `true` | +| `DISCOVERY_INTERVAL` | `--discovery-interval` | Device discovery interval | `5m` | ### Configuration Examples diff --git a/pkg/service/handlers/handlers_proxy.go b/pkg/service/handlers/handlers_proxy.go index 73d692a..69f26d5 100644 --- a/pkg/service/handlers/handlers_proxy.go +++ b/pkg/service/handlers/handlers_proxy.go @@ -35,6 +35,7 @@ func (s *Server) HandleProxyRequest(w http.ResponseWriter, r *http.Request) { lp := proxy.NewLoggingProxy(target.String(), s.proxyRedact) lp.LogBody = s.proxyLogBody + lp.RecordEnabled = s.recordEnabled lp.SetRecorder(s.recorder) proxy := httputil.NewSingleHostReverseProxy(target) diff --git a/pkg/service/handlers/handlers_setup.go b/pkg/service/handlers/handlers_setup.go index e62c1a0..0aa528f 100644 --- a/pkg/service/handlers/handlers_setup.go +++ b/pkg/service/handlers/handlers_setup.go @@ -351,6 +351,7 @@ func (s *Server) HandleGetProxySettings(w http.ResponseWriter, _ *http.Request) if err := json.NewEncoder(w).Encode(map[string]bool{ "redact": s.proxyRedact, "log_body": s.proxyLogBody, + "record": s.recordEnabled, }); err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return @@ -377,6 +378,7 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques var settings struct { 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) @@ -385,6 +387,7 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques s.proxyRedact = settings.Redact s.proxyLogBody = settings.LogBody + s.recordEnabled = settings.Record w.Header().Set("Content-Type", "application/json") diff --git a/pkg/service/handlers/recorder_middleware.go b/pkg/service/handlers/recorder_middleware.go index 2cc884d..312de46 100644 --- a/pkg/service/handlers/recorder_middleware.go +++ b/pkg/service/handlers/recorder_middleware.go @@ -12,7 +12,7 @@ import ( // RecordMiddleware returns a middleware that records "self" requests and responses. func (s *Server) RecordMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if s.recorder == nil { + if s.recorder == nil || !s.recordEnabled { next.ServeHTTP(w, r) return } diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go index 84a92a8..778ad9e 100644 --- a/pkg/service/handlers/server.go +++ b/pkg/service/handlers/server.go @@ -14,25 +14,27 @@ import ( // Server handles HTTP requests for the SoundTouch service. type Server struct { - ds *datastore.DataStore - sm *setup.Manager - serverURL string - proxyURL string - discovering bool - proxyRedact bool - proxyLogBody bool - recorder *proxy.Recorder + ds *datastore.DataStore + sm *setup.Manager + serverURL string + proxyURL string + discovering bool + proxyRedact bool + proxyLogBody bool + recordEnabled bool + recorder *proxy.Recorder } // NewServer creates a new SoundTouch service server. -func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody bool) *Server { +func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled bool) *Server { return &Server{ - ds: ds, - sm: sm, - serverURL: serverURL, - proxyURL: serverURL, - proxyRedact: proxyRedact, - proxyLogBody: proxyLogBody, + ds: ds, + sm: sm, + serverURL: serverURL, + proxyURL: serverURL, + proxyRedact: proxyRedact, + proxyLogBody: proxyLogBody, + recordEnabled: recordEnabled, } } @@ -41,6 +43,11 @@ func (s *Server) SetRecorder(r *proxy.Recorder) { s.recorder = r } +// GetRecordEnabled returns whether recording is enabled. +func (s *Server) GetRecordEnabled() bool { + return s.recordEnabled +} + // DiscoverDevices starts a background device discovery process. // //nolint:contextcheck diff --git a/pkg/service/handlers/web/index.html b/pkg/service/handlers/web/index.html index 02a7aa0..46531dc 100644 --- a/pkg/service/handlers/web/index.html +++ b/pkg/service/handlers/web/index.html @@ -194,6 +194,7 @@ Proxy Logging: + diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index 3f15e30..059d612 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -20,6 +20,7 @@ async function fetchProxySettings() { const settings = await response.json(); document.getElementById('proxy-redact').checked = settings.redact; document.getElementById('proxy-log-body').checked = settings.log_body; + document.getElementById('proxy-record').checked = settings.record; } catch (error) { console.error('Failed to fetch proxy settings', error); } @@ -28,7 +29,8 @@ async function fetchProxySettings() { async function updateProxySettings() { const settings = { redact: document.getElementById('proxy-redact').checked, - log_body: document.getElementById('proxy-log-body').checked + log_body: document.getElementById('proxy-log-body').checked, + record: document.getElementById('proxy-record').checked }; try { await fetch('/setup/proxy-settings', { @@ -127,7 +129,7 @@ function openTab(evt, tabId) { if (content) { content.className += " active"; } - + if (evt) { evt.currentTarget.className += " active"; } else { @@ -251,13 +253,13 @@ async function updateDeviceInfo(ip) { if (row) { const nameEl = row.querySelector('.col-name'); if (nameEl && info.name) nameEl.innerText = info.name; - + const modelEl = row.querySelector('.col-model'); if (modelEl && info.type) modelEl.innerText = info.type; - + const serialEl = row.querySelector('.col-serial'); if (serialEl && info.serialNumber) serialEl.innerText = info.serialNumber; - + const firmwareEl = row.querySelector('.col-firmware'); if (firmwareEl && info.softwareVersion) firmwareEl.innerText = info.softwareVersion; } @@ -308,13 +310,13 @@ async function showSummary(ip) { if (row) { const nameEl = row.querySelector('.col-name'); if (nameEl && summary.device_name) nameEl.innerText = summary.device_name; - + const modelEl = row.querySelector('.col-model'); if (modelEl && summary.device_model) modelEl.innerText = summary.device_model; - + const serialEl = row.querySelector('.col-serial'); if (serialEl && summary.device_serial) serialEl.innerText = summary.device_serial; - + const firmwareEl = row.querySelector('.col-firmware'); if (firmwareEl && summary.firmware_version) firmwareEl.innerText = summary.firmware_version; } diff --git a/pkg/service/proxy/proxy.go b/pkg/service/proxy/proxy.go index 8a415e0..71e485b 100644 --- a/pkg/service/proxy/proxy.go +++ b/pkg/service/proxy/proxy.go @@ -20,11 +20,12 @@ var sensitiveHeaders = []string{ // LoggingProxy wraps a ReverseProxy to provide instrumentation. type LoggingProxy struct { - Proxy *httputil.ReverseProxy - Redact bool - LogBody bool - MaxBodySize int64 - Recorder *Recorder + Proxy *httputil.ReverseProxy + Redact bool + LogBody bool + RecordEnabled bool + MaxBodySize int64 + Recorder *Recorder } // NewLoggingProxy creates a lightweight logger for HTTP requests/responses. @@ -89,7 +90,7 @@ func (lp *LoggingProxy) LogResponse(r *http.Response) { log.Printf("[PROXY_RES] %d %s\n Headers:\n%s\n Body: %s", r.StatusCode, r.Request.URL.String(), headers, bodyStr) - if lp.Recorder != nil { + if lp.Recorder != nil && lp.RecordEnabled { _ = lp.Recorder.Record("upstream", r.Request, r) } }