Allow toggling HTTP interaction recording via CLI, environment, and Web UI

This commit is contained in:
Tobias Gesellchen
2026-02-14 12:39:39 +01:00
parent 133c07fefa
commit 9be1c7d588
9 changed files with 71 additions and 44 deletions
+14 -3
View File
@@ -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)
}
+12 -11
View File
@@ -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://<hostname>:8000` |
| `HTTPS_SERVER_URL` | `--https-server-url` | External HTTPS URL | `https://<hostname>: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://<hostname>:8000` |
| `HTTPS_SERVER_URL` | `--https-server-url` | External HTTPS URL | `https://<hostname>: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
+1
View File
@@ -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)
+3
View File
@@ -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")
+1 -1
View File
@@ -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
}
+22 -15
View File
@@ -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
+1
View File
@@ -194,6 +194,7 @@
Proxy Logging:
<label><input type="checkbox" id="proxy-redact" onchange="updateProxySettings()"> Redact Sensitive Headers</label>
<label style="margin-left: 15px;"><input type="checkbox" id="proxy-log-body" onchange="updateProxySettings()"> Log Bodies</label>
<label style="margin-left: 15px;"><input type="checkbox" id="proxy-record" onchange="updateProxySettings()"> Record Interactions</label>
</div>
</div>
</div>
+10 -8
View File
@@ -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;
}
+7 -6
View File
@@ -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)
}
}