diff --git a/internal/daemon/autostart_darwin.go b/internal/daemon/autostart_darwin.go index 1367cc42..32be89e5 100644 --- a/internal/daemon/autostart_darwin.go +++ b/internal/daemon/autostart_darwin.go @@ -4,10 +4,12 @@ package daemon import ( "fmt" + "html" "os" "os/exec" "path/filepath" "strconv" + "strings" "text/template" ) @@ -53,6 +55,10 @@ const plistTemplate = ` PATH {{.EnvPath}} + {{- range $key, $val := .ExtraEnv}} + {{$key}} + {{xmlEscape $val}} + {{- end}} @@ -66,6 +72,7 @@ type plistData struct { Port int LogFile string EnvPath string + ExtraEnv map[string]string } // EnableAutostart creates the launchd plist and loads it. @@ -89,6 +96,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,9 +114,12 @@ func EnableAutostart(configPath string, port int) error { Port: port, LogFile: paths.LogFile, EnvPath: envPath, + 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 0d08677b..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 @@ -40,8 +42,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") @@ -50,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 {