From d13ef679ba3c4edd8469ae9d49ee6e02e5d8c3b7 Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Wed, 22 Jul 2026 14:15:43 +0200 Subject: [PATCH 1/2] fix: preserve ROUTATIC_PROXY_* env vars in launchd autostart The launchd plist template only preserved PATH, so ROUTATIC_PROXY_* environment variables (like ROUTATIC_PROXY_API_KEY) were lost when the proxy restarted on login. This caused the autostarted proxy to fail because it couldn't find the API key. Now all ROUTATIC_PROXY_* vars from the current environment are written into the plist's EnvironmentVariables dict so the proxy starts correctly after a reboot. --- internal/daemon/autostart_darwin.go | 18 ++++++++++++++++++ internal/daemon/autostart_darwin_test.go | 7 +++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/daemon/autostart_darwin.go b/internal/daemon/autostart_darwin.go index 1367cc42..03902b54 100644 --- a/internal/daemon/autostart_darwin.go +++ b/internal/daemon/autostart_darwin.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "strconv" + "strings" "text/template" ) @@ -53,6 +54,10 @@ const plistTemplate = ` PATH {{.EnvPath}} + {{- range $key, $val := .ExtraEnv}} + {{$key}} + {{$val}} + {{- end}} @@ -66,6 +71,7 @@ type plistData struct { Port int LogFile string EnvPath string + ExtraEnv map[string]string } // EnableAutostart creates the launchd plist and loads it. @@ -89,6 +95,17 @@ func EnableAutostart(configPath string, port int) error { envPath = "/usr/local/bin:/usr/bin:/bin" } + extraEnv := make(map[string]string) + for _, e := range os.Environ() { + k, v, ok := strings.Cut(e, "=") + if !ok { + continue + } + if strings.HasPrefix(k, "ROUTATIC_PROXY_") { + extraEnv[k] = v + } + } + data := plistData{ Label: LaunchAgent, BinaryPath: paths.BinaryPath, @@ -96,6 +113,7 @@ func EnableAutostart(configPath string, port int) error { Port: port, LogFile: paths.LogFile, EnvPath: envPath, + ExtraEnv: extraEnv, } tmpl, err := template.New("plist").Parse(plistTemplate) diff --git a/internal/daemon/autostart_darwin_test.go b/internal/daemon/autostart_darwin_test.go index 0d08677b..b8a46688 100644 --- a/internal/daemon/autostart_darwin_test.go +++ b/internal/daemon/autostart_darwin_test.go @@ -40,8 +40,11 @@ func TestEnableDisableAutostart_Darwin(t *testing.T) { if !strings.Contains(content, "Label\n com.routatic.proxy") { t.Errorf("Plist missing correct Label string") } - if !strings.Contains(content, "serve") { - t.Errorf("Plist missing serve command") + if !strings.Contains(content, "start") { + t.Errorf("Plist missing start command") + } + if !strings.Contains(content, "--background") { + t.Errorf("Plist missing --background flag") } if !strings.Contains(content, "--config\n /tmp/mock-config.json") { t.Errorf("Plist missing config path arguments") From dc27e2ccb65164e2c282604774f8f07e8820e21a Mon Sep 17 00:00:00 2001 From: Stefano Verna Date: Thu, 23 Jul 2026 10:58:37 +0200 Subject: [PATCH 2/2] fix: XML-escape env var values in launchd plist template text/template does not escape XML special characters, so a stray &, <, or > in an env var value would produce invalid plist XML. Added xmlEscape template function using html.EscapeString. Also added test coverage verifying ROUTATIC_PROXY_* vars with special characters are properly escaped in the generated plist. --- internal/daemon/autostart_darwin.go | 7 +++++-- internal/daemon/autostart_darwin_test.go | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/daemon/autostart_darwin.go b/internal/daemon/autostart_darwin.go index 03902b54..32be89e5 100644 --- a/internal/daemon/autostart_darwin.go +++ b/internal/daemon/autostart_darwin.go @@ -4,6 +4,7 @@ package daemon import ( "fmt" + "html" "os" "os/exec" "path/filepath" @@ -56,7 +57,7 @@ const plistTemplate = ` {{.EnvPath}} {{- range $key, $val := .ExtraEnv}} {{$key}} - {{$val}} + {{xmlEscape $val}} {{- end}} @@ -116,7 +117,9 @@ func EnableAutostart(configPath string, port int) error { ExtraEnv: extraEnv, } - tmpl, err := template.New("plist").Parse(plistTemplate) + tmpl, err := template.New("plist").Funcs(template.FuncMap{ + "xmlEscape": html.EscapeString, + }).Parse(plistTemplate) if err != nil { return fmt.Errorf("cannot parse plist template: %w", err) } diff --git a/internal/daemon/autostart_darwin_test.go b/internal/daemon/autostart_darwin_test.go index b8a46688..812a6209 100644 --- a/internal/daemon/autostart_darwin_test.go +++ b/internal/daemon/autostart_darwin_test.go @@ -13,6 +13,8 @@ func TestEnableDisableAutostart_Darwin(t *testing.T) { // Setup temporary home directory tempHome := t.TempDir() t.Setenv("HOME", tempHome) + // Set a ROUTATIC_PROXY_* var with characters that need XML escaping. + t.Setenv("ROUTATIC_PROXY_API_KEY", "sk-test&key") configPath := "/tmp/mock-config.json" port := 9999 @@ -53,6 +55,15 @@ func TestEnableDisableAutostart_Darwin(t *testing.T) { t.Errorf("Plist missing port arguments") } + // Verify ROUTATIC_PROXY_* env var is present and XML-escaped. + if !strings.Contains(content, "ROUTATIC_PROXY_API_KEY") { + t.Errorf("Plist missing ROUTATIC_PROXY_API_KEY env var") + } + // The value should be XML-escaped: & -> &, < -> <, > -> > + if !strings.Contains(content, "sk-test&key<value>") { + t.Errorf("Plist missing XML-escaped API key value. Content:\n%s", content) + } + // Disable autostart err = DisableAutostart() if err != nil {