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
|
- Automatic certificate management
|
||||||
- Cross-platform support (Windows, macOS, Linux)
|
- Cross-platform support (Windows, macOS, Linux)
|
||||||
- System proxy configuration
|
- System proxy configuration
|
||||||
|
- Automatic admin prompt on macOS/Linux for certificate/proxy setup
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ go build
|
|||||||
|
|
||||||
- `-r`: Redirect target host (default: "127.0.0.1:21000")
|
- `-r`: Redirect target host (default: "127.0.0.1:21000")
|
||||||
- `-b`: Comma-separated list of blocked ports
|
- `-b`: Comma-separated list of blocked ports
|
||||||
|
- `-p`: Proxy listen port (default: auto)
|
||||||
- `-e`: Path to an executable to run with admin privileges
|
- `-e`: Path to an executable to run with admin privileges
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
@@ -66,6 +68,14 @@ go build
|
|||||||
./firefly-proxy.exe -e "/path/to/your/executable" //windows
|
./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
|
## How it works
|
||||||
|
|
||||||
The proxy intercepts HTTP/HTTPS traffic and can:
|
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)
|
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() {
|
func main() {
|
||||||
redirectHost := flag.String("r", "127.0.0.1:21000", "redirect target host")
|
redirectHost := flag.String("r", "127.0.0.1:21000", "redirect target host")
|
||||||
blockedStr := flag.String("b", "", "comma separated list of blocked ports")
|
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")
|
exePath := flag.String("e", "", "path to the executable")
|
||||||
flag.Parse()
|
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)
|
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" {
|
if port == "-1" {
|
||||||
zlog.Error().Str("port", port).Msg("No free port available")
|
zlog.Error().Str("port", port).Msg("No free port available")
|
||||||
return
|
return
|
||||||
@@ -39,6 +72,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
proxyAddr := "127.0.0.1"
|
proxyAddr := "127.0.0.1"
|
||||||
|
proxyEndpoint := proxyAddr + ":" + port
|
||||||
proxyEnabled := false
|
proxyEnabled := false
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -84,6 +118,10 @@ func main() {
|
|||||||
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
||||||
host := req.URL.Hostname()
|
host := req.URL.Hostname()
|
||||||
path := req.URL.Path
|
path := req.URL.Path
|
||||||
|
rawQuery := req.URL.RawQuery
|
||||||
|
if rawQuery == "" {
|
||||||
|
rawQuery = rawQueryFromRequestURI(req.RequestURI)
|
||||||
|
}
|
||||||
|
|
||||||
if matchDomain(host, AlwaysIgnoreDomains) {
|
if matchDomain(host, AlwaysIgnoreDomains) {
|
||||||
return req, nil
|
return req, nil
|
||||||
@@ -101,18 +139,31 @@ func main() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
full := req.URL.String()
|
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.Scheme = "http"
|
||||||
req.URL.Host = *redirectHost
|
req.URL.Host = *redirectHost
|
||||||
|
req.URL.RawQuery = rawQuery
|
||||||
|
req.RequestURI = ""
|
||||||
|
zlog.Info().Str("to_url", req.URL.String()).Msg("Force redirected")
|
||||||
return req, nil
|
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.Scheme = "http"
|
||||||
req.URL.Host = *redirectHost
|
req.URL.Host = *redirectHost
|
||||||
|
req.URL.RawQuery = rawQuery
|
||||||
|
req.RequestURI = ""
|
||||||
|
zlog.Info().Str("to_url", req.URL.String()).Msg("Redirected domain")
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +196,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
zlog.Info().
|
zlog.Info().
|
||||||
Str("ProxyAddress", proxyAddr).
|
Str("ProxyAddress", proxyEndpoint).
|
||||||
Str("RedirectTo", *redirectHost).
|
Str("RedirectTo", *redirectHost).
|
||||||
Str("BlockedPorts", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(blockedPorts)), ","), "[]")).
|
Str("BlockedPorts", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(blockedPorts)), ","), "[]")).
|
||||||
Str("ExePath", *exePath).
|
Str("ExePath", *exePath).
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tag": "1.1-02",
|
"tag": "1.1-03",
|
||||||
"title": "PreBuild Version 1.1 - 02"
|
"title": "PreBuild Version 1.1 - 03"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,6 +26,15 @@ func matchURL(url string, list []string) bool {
|
|||||||
return false
|
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 {
|
func cleanHost(h string) string {
|
||||||
if idx := strings.LastIndex(h, ":"); idx != -1 {
|
if idx := strings.LastIndex(h, ":"); idx != -1 {
|
||||||
return h[:idx]
|
return h[:idx]
|
||||||
|
|||||||
Reference in New Issue
Block a user