UPDATE: Add admin relaunch functionality for macOS and Linux, enhance README with new features and usage examples
Build and Release / release (push) Successful in 18s

This commit is contained in:
2026-05-23 19:53:13 +07:00
parent 1692bd73a2
commit 1abf0caee4
7 changed files with 204 additions and 7 deletions
+10
View File
@@ -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:
+46
View File
@@ -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
}
+73
View File
@@ -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)
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin && !linux
// +build !darwin,!linux
package main
func relaunchWithAdminIfNeeded() (bool, error) {
return false, nil
}
+56 -5
View File
@@ -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).
+2 -2
View File
@@ -1,5 +1,5 @@
{
"tag": "1.1-02",
"title": "PreBuild Version 1.1 - 02"
"tag": "1.1-03",
"title": "PreBuild Version 1.1 - 03"
}
+9
View File
@@ -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]