From 1abf0caee42d6e8d1db82c9da482ed5c4af2aed0 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Sat, 23 May 2026 19:53:13 +0700 Subject: [PATCH] UPDATE: Add admin relaunch functionality for macOS and Linux, enhance README with new features and usage examples --- README.md | 10 ++++++ admin_self_darwin.go | 46 ++++++++++++++++++++++++++++ admin_self_linux.go | 73 ++++++++++++++++++++++++++++++++++++++++++++ admin_self_other.go | 8 +++++ main.go | 61 +++++++++++++++++++++++++++++++++--- script/release.json | 4 +-- utils.go | 9 ++++++ 7 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 admin_self_darwin.go create mode 100644 admin_self_linux.go create mode 100644 admin_self_other.go diff --git a/README.md b/README.md index e41dac6..53dac62 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A lightweight HTTP/HTTPS proxy server with domain redirection and request blocki - Automatic certificate management - Cross-platform support (Windows, macOS, Linux) - System proxy configuration +- Automatic admin prompt on macOS/Linux for certificate/proxy setup ## Installation @@ -38,6 +39,7 @@ go build - `-r`: Redirect target host (default: "127.0.0.1:21000") - `-b`: Comma-separated list of blocked ports +- `-p`: Proxy listen port (default: auto) - `-e`: Path to an executable to run with admin privileges ### Examples @@ -66,6 +68,14 @@ go build ./firefly-proxy.exe -e "/path/to/your/executable" //windows ``` +5. Start proxy on a specific port: + ```bash + ./firefly-proxy -p 8888 //linux|macos + ./firefly-proxy.exe -p 8888 //windows + ``` + + On macOS/Linux, if the proxy is not already running as root, it relaunches with an administrator prompt. On Linux, logs from the elevated process are written to `/tmp/firefly-go-proxy.log`; on macOS, elevated process output is discarded. + ## How it works The proxy intercepts HTTP/HTTPS traffic and can: diff --git a/admin_self_darwin.go b/admin_self_darwin.go new file mode 100644 index 0000000..153a458 --- /dev/null +++ b/admin_self_darwin.go @@ -0,0 +1,46 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func relaunchWithAdminIfNeeded() (bool, error) { + if os.Geteuid() == 0 { + return false, nil + } + + exePath, err := os.Executable() + if err != nil { + return false, fmt.Errorf("get executable path: %w", err) + } + + workDir, err := os.Getwd() + if err != nil { + return false, fmt.Errorf("get working directory: %w", err) + } + + args := make([]string, 0, len(os.Args)) + args = append(args, shellQuote(exePath)) + for _, arg := range os.Args[1:] { + args = append(args, shellQuote(arg)) + } + + command := fmt.Sprintf( + "cd %s && %s > /dev/null 2>&1 &", + shellQuote(workDir), + strings.Join(args, " "), + ) + script := fmt.Sprintf("do shell script %s with administrator privileges", appleScriptString(command)) + + if out, err := exec.Command("osascript", "-e", script).CombinedOutput(); err != nil { + return false, formatCommandError("relaunch proxy as admin", err, out) + } + + return true, nil +} diff --git a/admin_self_linux.go b/admin_self_linux.go new file mode 100644 index 0000000..97d72c2 --- /dev/null +++ b/admin_self_linux.go @@ -0,0 +1,73 @@ +//go:build linux +// +build linux + +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func relaunchWithAdminIfNeeded() (bool, error) { + if os.Geteuid() == 0 { + return false, nil + } + + exePath, err := os.Executable() + if err != nil { + return false, fmt.Errorf("get executable path: %w", err) + } + + workDir, err := os.Getwd() + if err != nil { + return false, fmt.Errorf("get working directory: %w", err) + } + + args := make([]string, 0, len(os.Args)) + args = append(args, shellQuote(exePath)) + for _, arg := range os.Args[1:] { + args = append(args, shellQuote(arg)) + } + + logPath := filepath.Join(os.TempDir(), "firefly-go-proxy.log") + command := fmt.Sprintf( + "cd %s && nohup %s >> %s 2>&1 &", + shellQuote(workDir), + strings.Join(args, " "), + shellQuote(logPath), + ) + + if pkexecPath, err := exec.LookPath("pkexec"); err == nil { + if out, err := exec.Command(pkexecPath, "sh", "-c", command).CombinedOutput(); err == nil { + return true, nil + } else if _, sudoErr := exec.LookPath("sudo"); sudoErr != nil { + return false, formatCommandError("relaunch proxy as admin with pkexec", err, out) + } + } + + sudoPath, err := exec.LookPath("sudo") + if err != nil { + return false, errors.New("pkexec or sudo is required to relaunch with admin privileges") + } + if out, err := exec.Command(sudoPath, "sh", "-c", command).CombinedOutput(); err != nil { + return false, formatCommandError("relaunch proxy as admin with sudo", err, out) + } + + return true, nil +} + +func shellQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" +} + +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) +} diff --git a/admin_self_other.go b/admin_self_other.go new file mode 100644 index 0000000..f503212 --- /dev/null +++ b/admin_self_other.go @@ -0,0 +1,8 @@ +//go:build !darwin && !linux +// +build !darwin,!linux + +package main + +func relaunchWithAdminIfNeeded() (bool, error) { + return false, nil +} diff --git a/main.go b/main.go index 5d568f6..ca6f917 100644 --- a/main.go +++ b/main.go @@ -19,14 +19,47 @@ import ( var ENV_CONFIG = make([]string, 0) +func rawQueryFromRequestURI(requestURI string) string { + queryStart := strings.IndexByte(requestURI, '?') + if queryStart == -1 { + return "" + } + + rawQuery := requestURI[queryStart+1:] + if fragmentStart := strings.IndexByte(rawQuery, '#'); fragmentStart != -1 { + rawQuery = rawQuery[:fragmentStart] + } + return rawQuery +} + func main() { redirectHost := flag.String("r", "127.0.0.1:21000", "redirect target host") blockedStr := flag.String("b", "", "comma separated list of blocked ports") + proxyPort := flag.Int("p", 0, "proxy listen port (default: auto)") exePath := flag.String("e", "", "path to the executable") flag.Parse() + relaunched, err := relaunchWithAdminIfNeeded() + if err != nil { + zlog.Error().Err(err).Msg("Failed to relaunch with admin privileges") + return + } + if relaunched { + zlog.Info().Msg("Relaunched with admin privileges") + return + } + blockedPorts := parseBlockedPorts(*blockedStr) - port := findFreePort(blockedPorts) + port := "" + if *proxyPort != 0 { + if *proxyPort < 1 || *proxyPort > 65535 { + zlog.Error().Int("port", *proxyPort).Msg("Invalid proxy port") + return + } + port = fmt.Sprint(*proxyPort) + } else { + port = findFreePort(blockedPorts) + } if port == "-1" { zlog.Error().Str("port", port).Msg("No free port available") return @@ -39,6 +72,7 @@ func main() { } addr := ":" + port proxyAddr := "127.0.0.1" + proxyEndpoint := proxyAddr + ":" + port proxyEnabled := false defer func() { @@ -84,6 +118,10 @@ func main() { proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { host := req.URL.Hostname() path := req.URL.Path + rawQuery := req.URL.RawQuery + if rawQuery == "" { + rawQuery = rawQueryFromRequestURI(req.RequestURI) + } if matchDomain(host, AlwaysIgnoreDomains) { return req, nil @@ -101,18 +139,31 @@ func main() { ) } full := req.URL.String() - if matchURL(full, ForceRedirectOnUrlContains) { + if containsURL(full, ForceRedirectOnUrlContains) { - zlog.Info().Str("Url", full).Msg("Force redirect") + zlog.Info(). + Str("from_url", full). + Str("raw_query", rawQuery). + Msg("Force redirect") req.URL.Scheme = "http" req.URL.Host = *redirectHost + req.URL.RawQuery = rawQuery + req.RequestURI = "" + zlog.Info().Str("to_url", req.URL.String()).Msg("Force redirected") return req, nil } - zlog.Info().Str("Host", host).Msg("Redirect domain") + zlog.Info(). + Str("host", host). + Str("from_url", full). + Str("raw_query", rawQuery). + Msg("Redirect domain") req.URL.Scheme = "http" req.URL.Host = *redirectHost + req.URL.RawQuery = rawQuery + req.RequestURI = "" + zlog.Info().Str("to_url", req.URL.String()).Msg("Redirected domain") return req, nil } @@ -145,7 +196,7 @@ func main() { } go func() { zlog.Info(). - Str("ProxyAddress", proxyAddr). + Str("ProxyAddress", proxyEndpoint). Str("RedirectTo", *redirectHost). Str("BlockedPorts", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(blockedPorts)), ","), "[]")). Str("ExePath", *exePath). diff --git a/script/release.json b/script/release.json index 717531f..c1e4f9e 100644 --- a/script/release.json +++ b/script/release.json @@ -1,5 +1,5 @@ { - "tag": "1.1-02", - "title": "PreBuild Version 1.1 - 02" + "tag": "1.1-03", + "title": "PreBuild Version 1.1 - 03" } \ No newline at end of file diff --git a/utils.go b/utils.go index 870d8ef..d00970f 100644 --- a/utils.go +++ b/utils.go @@ -26,6 +26,15 @@ func matchURL(url string, list []string) bool { return false } +func containsURL(url string, list []string) bool { + for _, u := range list { + if strings.Contains(url, u) { + return true + } + } + return false +} + func cleanHost(h string) string { if idx := strings.LastIndex(h, ":"); idx != -1 { return h[:idx]