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:
Tobias Gesellchen
2026-02-15 21:51:55 +01:00
parent d616bc09fd
commit 89bafd97b6
9 changed files with 315 additions and 180 deletions
+135 -86
View File
@@ -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
}
+10 -9
View File
@@ -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.
+1 -1
View File
@@ -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,
+58 -36
View File
@@ -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()
+3 -3
View File
@@ -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)
}
}
+31 -29
View File
@@ -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.
+4 -3
View File
@@ -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>
+12 -4
View File
@@ -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...';
+61 -9
View File
@@ -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) {