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
Build and Release / release (push) Successful in 18s
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build !darwin && !linux
|
||||
// +build !darwin,!linux
|
||||
|
||||
package main
|
||||
|
||||
func relaunchWithAdminIfNeeded() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"tag": "1.1-02",
|
||||
"title": "PreBuild Version 1.1 - 02"
|
||||
"tag": "1.1-03",
|
||||
"title": "PreBuild Version 1.1 - 03"
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user