From 211912d44b0bd09a224a6809d6a2e018e0ba7f3c Mon Sep 17 00:00:00 2001 From: AzenKain Date: Sat, 16 May 2026 09:23:36 +0700 Subject: [PATCH] UPDATE: Enhance macOS build and proxy management functionality --- .gitignore | 3 +- Makefile | 21 +++- cert_darwin.go | 63 +++++++++++- main.go | 27 +++-- run_admin_darwin.go | 34 ++++++- system_proxy_darwin.go | 225 +++++++++++++++++++++++++++++++++-------- 6 files changed, 315 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 07d8d9f..eba5060 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .history .vscode *.exe +firefly-go-proxy-darwin-* *.pem *.log -*.crt \ No newline at end of file +*.crt diff --git a/Makefile b/Makefile index 84b52cc..058b779 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,27 @@ +.PHONY: build build_mac build_mac_amd64 build_mac_arm64 build_ico set_logo + build: @echo Building windows binary... set GOOS=windows&& set GOARCH=amd64&& set CGO_ENABLED=0&& go build -trimpath -ldflags="-s -w" . @echo Done! +build_mac: build_mac_amd64 build_mac_arm64 + @echo Done! + +build_mac_amd64: export GOOS=darwin +build_mac_amd64: export GOARCH=amd64 +build_mac_amd64: export CGO_ENABLED=0 +build_mac_amd64: + @echo Building macOS amd64 binary... + go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-darwin-amd64 . + +build_mac_arm64: export GOOS=darwin +build_mac_arm64: export GOARCH=arm64 +build_mac_arm64: export CGO_ENABLED=0 +build_mac_arm64: + @echo Building macOS arm64 binary... + go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-darwin-arm64 . + build_ico: @echo Building application icon... magick logo.jpg -define icon:auto-resize=256,128,64,48,32,16 ./logo.ico @@ -11,4 +30,4 @@ build_ico: set_logo: @echo Embedding application icon... go-winres simply --icon ./logo.ico - @echo Done! \ No newline at end of file + @echo Done! diff --git a/cert_darwin.go b/cert_darwin.go index 7fd1eec..b09fd57 100644 --- a/cert_darwin.go +++ b/cert_darwin.go @@ -4,22 +4,79 @@ package main import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + "os" "os/exec" ) +const darwinSystemKeychain = "/Library/Keychains/System.keychain" + func installCA(absPath string) error { + cert, err := readCertificate(absPath) + if err != nil { + return err + } + + exists, err := certificateExistsInKeychain(cert, darwinSystemKeychain) + if err != nil { + return err + } + if exists { + return nil + } + cmd := exec.Command( "security", "add-trusted-cert", "-d", "-r", "trustRoot", - "-k", "/Library/Keychains/System.keychain", + "-k", darwinSystemKeychain, absPath, ) - if err := cmd.Run(); err != nil { - return err + if out, err := cmd.CombinedOutput(); err != nil { + return formatCommandError("install CA into macOS system keychain", err, out) } return nil } + +func readCertificate(path string) (*x509.Certificate, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read CA certificate: %w", err) + } + + if block, _ := pem.Decode(data); block != nil { + data = block.Bytes + } + + cert, err := x509.ParseCertificate(data) + if err != nil { + return nil, fmt.Errorf("parse CA certificate: %w", err) + } + + return cert, nil +} + +func certificateExistsInKeychain(cert *x509.Certificate, keychain string) (bool, error) { + out, err := exec.Command("security", "find-certificate", "-a", "-p", keychain).CombinedOutput() + if err != nil { + return false, formatCommandError("read macOS system keychain", err, out) + } + + remaining := out + for { + block, rest := pem.Decode(remaining) + if block == nil { + return false, nil + } + if block.Type == "CERTIFICATE" && bytes.Equal(block.Bytes, cert.Raw) { + return true, nil + } + remaining = rest + } +} diff --git a/main.go b/main.go index 8402143..5d568f6 100644 --- a/main.go +++ b/main.go @@ -39,18 +39,26 @@ func main() { } addr := ":" + port proxyAddr := "127.0.0.1" + proxyEnabled := false defer func() { if r := recover(); r != nil { zlog.Error(). Interface("panic", r). - Msg("Unexpected panic, resetting system proxy") - - setProxy(false, "", "") + Msg("Unexpected panic") + } + if proxyEnabled { + if err := setProxy(false, "", ""); err != nil { + zlog.Error().Err(err).Msg("Failed to reset system proxy") + } } }() - setProxy(true, proxyAddr, port) + if err := setProxy(true, proxyAddr, port); err != nil { + zlog.Error().Err(err).Msg("Failed to set system proxy") + return + } + proxyEnabled = true customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)} var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { @@ -122,6 +130,7 @@ func main() { } stop := make(chan os.Signal, 1) + serverErr := make(chan error, 1) signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) if *exePath != "" && exists(*exePath) { go func() { @@ -142,15 +151,19 @@ func main() { Str("ExePath", *exePath). Msg("Proxy started") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - zlog.Fatal().Err(err).Msg("ListenAndServe failed") + serverErr <- err } }() - <-stop + select { + case <-stop: + case err := <-serverErr: + zlog.Error().Err(err).Msg("ListenAndServe failed") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { zlog.Error().Err(err).Msg("Server shutdown error") } - setProxy(false, "", "") } diff --git a/run_admin_darwin.go b/run_admin_darwin.go index 7da6101..75138c9 100644 --- a/run_admin_darwin.go +++ b/run_admin_darwin.go @@ -5,16 +5,40 @@ package main import ( "fmt" - "os" "os/exec" "strings" ) func runWithAdmin(exePath string, env []string) error { - escaped := strings.ReplaceAll(exePath, `"`, `\"`) - script := fmt.Sprintf(`do shell script "%s" with administrator privileges`, escaped) + command := shellQuote(exePath) + if len(env) > 0 { + command = strings.Join(shellEnvAssignments(env), " ") + " " + command + } + command += " >/dev/null 2>&1 &" + script := fmt.Sprintf("do shell script %s with administrator privileges", appleScriptString(command)) cmd := exec.Command("osascript", "-e", script) - cmd.Env = append(os.Environ(), env...) - return cmd.Start() + return cmd.Run() +} + +func shellEnvAssignments(env []string) []string { + assignments := make([]string, 0, len(env)) + for _, value := range env { + key, val, ok := strings.Cut(value, "=") + if !ok || key == "" { + continue + } + assignments = append(assignments, key+"="+shellQuote(val)) + } + return assignments +} + +func shellQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" +} + +func appleScriptString(value string) string { + value = strings.ReplaceAll(value, `\`, `\\`) + value = strings.ReplaceAll(value, `"`, `\"`) + return `"` + value + `"` } diff --git a/system_proxy_darwin.go b/system_proxy_darwin.go index e4d70b3..1937efc 100644 --- a/system_proxy_darwin.go +++ b/system_proxy_darwin.go @@ -4,65 +4,208 @@ package main import ( + "errors" "fmt" "os/exec" "strings" + "sync" ) -func parseNetworkServices(out string) []string { - lines := strings.Split(out, "\n") - var result []string - - for _, line := range lines { - if strings.Contains(line, "(Hardware Port:") { - start := strings.Index(line, "Hardware Port: ") + len("Hardware Port: ") - end := strings.Index(line[start:], ",") - if end > 0 { - result = append(result, line[start:start+end]) - } - } - } - return result +type darwinProxyEndpoint struct { + enabled bool + server string + port string } -func contains(arr []string, v string) bool { - for _, x := range arr { - if x == v { - return true +type darwinProxySettings struct { + web darwinProxyEndpoint + secure darwinProxyEndpoint +} + +var darwinProxyState = struct { + sync.Mutex + captured bool + previous map[string]darwinProxySettings +}{} + +func enabledNetworkServices() ([]string, error) { + out, err := exec.Command("networksetup", "-listallnetworkservices").CombinedOutput() + if err != nil { + return nil, formatCommandError("list network services", err, out) + } + + var services []string + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "An asterisk") || strings.HasPrefix(line, "*") { + continue + } + services = append(services, line) + } + + if len(services) == 0 { + return nil, fmt.Errorf("no enabled network services found") + } + + return services, nil +} + +func getProxySettings(service string) (darwinProxySettings, error) { + web, webErr := getProxyEndpoint("-getwebproxy", service) + secure, secureErr := getProxyEndpoint("-getsecurewebproxy", service) + return darwinProxySettings{ + web: web, + secure: secure, + }, errors.Join(webErr, secureErr) +} + +func getProxyEndpoint(flag string, service string) (darwinProxyEndpoint, error) { + out, err := exec.Command("networksetup", flag, service).CombinedOutput() + if err != nil { + return darwinProxyEndpoint{}, formatCommandError(flag+" "+service, err, out) + } + + values := make(map[string]string) + for _, line := range strings.Split(string(out), "\n") { + key, value, ok := strings.Cut(line, ":") + if ok { + values[strings.TrimSpace(key)] = strings.TrimSpace(value) } } - return false + + return darwinProxyEndpoint{ + enabled: values["Enabled"] == "Yes", + server: values["Server"], + port: values["Port"], + }, nil +} + +func setProxyForServices(services []string, host string, port string) error { + var errs []error + for _, service := range services { + errs = append(errs, + runNetworkSetup("-setwebproxy", service, host, port), + runNetworkSetup("-setsecurewebproxy", service, host, port), + runNetworkSetup("-setwebproxystate", service, "on"), + runNetworkSetup("-setsecurewebproxystate", service, "on"), + ) + } + + return errors.Join(errs...) +} + +func restoreProxySettings(settings map[string]darwinProxySettings) error { + var errs []error + for service, setting := range settings { + if setting.web.server != "" && setting.web.port != "" { + errs = append(errs, runNetworkSetup("-setwebproxy", service, setting.web.server, setting.web.port)) + } + errs = append(errs, runNetworkSetup("-setwebproxystate", service, proxyState(setting.web.enabled))) + + if setting.secure.server != "" && setting.secure.port != "" { + errs = append(errs, runNetworkSetup("-setsecurewebproxy", service, setting.secure.server, setting.secure.port)) + } + errs = append(errs, runNetworkSetup("-setsecurewebproxystate", service, proxyState(setting.secure.enabled))) + } + + return errors.Join(errs...) +} + +func disableProxyForServices(services []string) error { + var errs []error + for _, service := range services { + errs = append(errs, + runNetworkSetup("-setwebproxystate", service, "off"), + runNetworkSetup("-setsecurewebproxystate", service, "off"), + ) + } + + return errors.Join(errs...) +} + +func proxyState(enabled bool) string { + if enabled { + return "on" + } + return "off" +} + +func runNetworkSetup(args ...string) error { + out, err := exec.Command("networksetup", args...).CombinedOutput() + if err != nil { + return formatCommandError("networksetup "+strings.Join(args, " "), err, out) + } + return nil +} + +func formatCommandError(action string, err error, out []byte) error { + msg := strings.TrimSpace(string(out)) + if msg == "" { + return fmt.Errorf("%s: %w", action, err) + } + return fmt.Errorf("%s: %w: %s", action, err, msg) +} + +func captureProxySettings(services []string) (map[string]darwinProxySettings, error) { + settings := make(map[string]darwinProxySettings, len(services)) + var errs []error + + for _, service := range services { + proxySettings, err := getProxySettings(service) + if err != nil { + errs = append(errs, err) + continue + } + settings[service] = proxySettings + } + + if err := errors.Join(errs...); err != nil { + return nil, err + } + + return settings, nil } func setProxy(enable bool, host string, port string) error { - out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput() + services, err := enabledNetworkServices() if err != nil { return err } - services := parseNetworkServices(string(out)) - active := "" - - if contains(services, "Wi-Fi") { - active = "Wi-Fi" - } else if contains(services, "Ethernet") { - active = "Ethernet" - } else { - if len(services) == 0 { - return fmt.Errorf("no network services found") - } - active = services[0] - } + darwinProxyState.Lock() + defer darwinProxyState.Unlock() if enable { - exec.Command("networksetup", "-setwebproxy", active, host, port).Run() - exec.Command("networksetup", "-setsecurewebproxy", active, host, port).Run() - exec.Command("networksetup", "-setwebproxystate", active, "on").Run() - exec.Command("networksetup", "-setsecurewebproxystate", active, "on").Run() - } else { - exec.Command("networksetup", "-setwebproxystate", active, "off").Run() - exec.Command("networksetup", "-setsecurewebproxystate", active, "off").Run() + if host == "" || port == "" { + return fmt.Errorf("host and port are required to enable proxy") + } + + if !darwinProxyState.captured { + settings, err := captureProxySettings(services) + if err != nil { + return err + } + darwinProxyState.previous = settings + darwinProxyState.captured = true + } + + if err := setProxyForServices(services, host, port); err != nil { + restoreErr := restoreProxySettings(darwinProxyState.previous) + darwinProxyState.previous = nil + darwinProxyState.captured = false + return errors.Join(err, restoreErr) + } + return nil } - return nil + if darwinProxyState.captured { + err := restoreProxySettings(darwinProxyState.previous) + if err == nil { + darwinProxyState.previous = nil + darwinProxyState.captured = false + } + return err + } + + return disableProxyForServices(services) }