Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 321c462f92 | |||
| d3ac27aa5d | |||
| 1abf0caee4 | |||
| 1692bd73a2 | |||
| 211912d44b | |||
| 24b28cdcca | |||
| 50d489dabe |
@@ -20,13 +20,45 @@ jobs:
|
|||||||
- name: Build for Windows
|
- name: Build for Windows
|
||||||
run: GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" .
|
run: GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" .
|
||||||
|
|
||||||
|
- name: Build for macOS Intel
|
||||||
|
run: |
|
||||||
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
|
||||||
|
go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o firefly-go-proxy-macos-amd64 .
|
||||||
|
|
||||||
|
- name: Build for macOS Apple Silicon
|
||||||
|
run: |
|
||||||
|
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 \
|
||||||
|
go build -trimpath -ldflags="-s -w" \
|
||||||
|
-o firefly-go-proxy-macos-arm64 .
|
||||||
|
|
||||||
- name: Grant execute permissions
|
- name: Grant execute permissions
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./script/release-uploader
|
chmod +x ./script/release-uploader
|
||||||
|
|
||||||
- name: Upload release
|
- name: Upload Windows release
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
run: script/release-uploader -token=$REPO_TOKEN -release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Go_Proxy/releases" -files="firefly-go-proxy.exe"
|
run: |
|
||||||
|
script/release-uploader \
|
||||||
|
-token="$REPO_TOKEN" \
|
||||||
|
-release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" \
|
||||||
|
-files="firefly-go-proxy.exe"
|
||||||
|
|
||||||
|
- name: Upload macOS Intel release
|
||||||
|
env:
|
||||||
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
|
run: |
|
||||||
|
script/release-uploader \
|
||||||
|
-token="$REPO_TOKEN" \
|
||||||
|
-release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" \
|
||||||
|
-files="firefly-go-proxy-macos-amd64"
|
||||||
|
|
||||||
|
- name: Upload macOS ARM release
|
||||||
|
env:
|
||||||
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
|
run: |
|
||||||
|
script/release-uploader \
|
||||||
|
-token="$REPO_TOKEN" \
|
||||||
|
-release-url="https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases" \
|
||||||
|
-files="firefly-go-proxy-macos-arm64"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
.history
|
.history
|
||||||
.vscode
|
.vscode
|
||||||
*.exe
|
*.exe
|
||||||
|
firefly-go-proxy-darwin-*
|
||||||
*.pem
|
*.pem
|
||||||
*.log
|
*.log
|
||||||
*.crt
|
*.crt
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
|
.PHONY: build build_mac build_mac_amd64 build_mac_arm64 build_ico set_logo
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@echo Building windows binary...
|
@echo Building windows binary...
|
||||||
set GOOS=windows&& set GOARCH=amd64&& set CGO_ENABLED=0&& go build -trimpath -ldflags="-s -w" .
|
set GOOS=windows&& set GOARCH=amd64&& set CGO_ENABLED=0&& go build -trimpath -ldflags="-s -w" .
|
||||||
@echo Done!
|
@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:
|
build_ico:
|
||||||
@echo Building application icon...
|
@echo Building application icon...
|
||||||
magick logo.jpg -define icon:auto-resize=256,128,64,48,32,16 ./logo.ico
|
magick logo.jpg -define icon:auto-resize=256,128,64,48,32,16 ./logo.ico
|
||||||
|
|||||||
@@ -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,77 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapperPID := os.Getpid()
|
||||||
|
launcherPID := os.Getppid()
|
||||||
|
|
||||||
|
args := make([]string, 0, len(os.Args))
|
||||||
|
args = append(args, shellQuote(exePath))
|
||||||
|
for _, arg := range os.Args[1:] {
|
||||||
|
args = append(args, shellQuote(arg))
|
||||||
|
}
|
||||||
|
if !hasFlagArg("parent-pid") {
|
||||||
|
args = append(args, shellQuote("-parent-pid"), shellQuote(strconv.Itoa(wrapperPID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForRelaunchedProxyShutdown(launcherPID)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForRelaunchedProxyShutdown(launcherPID int) {
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
defer signal.Stop(stop)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
case <-parentProcessDone(launcherPID):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasFlagArg(name string) bool {
|
||||||
|
for _, arg := range os.Args[1:] {
|
||||||
|
trimmed := strings.TrimLeft(arg, "-")
|
||||||
|
if trimmed == name || strings.HasPrefix(trimmed, name+"=") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+60
-3
@@ -4,22 +4,79 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const darwinSystemKeychain = "/Library/Keychains/System.keychain"
|
||||||
|
|
||||||
func installCA(absPath string) error {
|
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(
|
cmd := exec.Command(
|
||||||
"security",
|
"security",
|
||||||
"add-trusted-cert",
|
"add-trusted-cert",
|
||||||
"-d",
|
"-d",
|
||||||
"-r", "trustRoot",
|
"-r", "trustRoot",
|
||||||
"-k", "/Library/Keychains/System.keychain",
|
"-k", darwinSystemKeychain,
|
||||||
absPath,
|
absPath,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
return err
|
return formatCommandError("install CA into macOS system keychain", err, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
module firefly-go-proxy
|
module firefly-go-proxy
|
||||||
|
|
||||||
go 1.25.5
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/elazarl/goproxy v1.7.2
|
github.com/elazarl/goproxy v1.8.3
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.35.0
|
||||||
golang.org/x/sys v0.39.0
|
golang.org/x/sys v0.42.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/elazarl/goproxy v1.8.3 h1:XhiZpzW0NvsGOqSv/F3v4+1F29842yYaJNN+In5Fnuc=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/elazarl/goproxy v1.8.3/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -19,14 +19,48 @@ 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")
|
||||||
|
parentPID := flag.Int("parent-pid", 0, "parent process id to watch")
|
||||||
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,18 +73,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
proxyAddr := "127.0.0.1"
|
proxyAddr := "127.0.0.1"
|
||||||
|
proxyEndpoint := proxyAddr + ":" + port
|
||||||
|
proxyEnabled := false
|
||||||
|
stopProxyRefresh := func() {}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
stopProxyRefresh()
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
zlog.Error().
|
zlog.Error().
|
||||||
Interface("panic", r).
|
Interface("panic", r).
|
||||||
Msg("Unexpected panic, resetting system proxy")
|
Msg("Unexpected panic")
|
||||||
|
}
|
||||||
setProxy(false, "", "")
|
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
|
||||||
|
stopProxyRefresh = startProxyRefreshLoop(proxyAddr, port)
|
||||||
|
|
||||||
customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)}
|
customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)}
|
||||||
var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
||||||
@@ -76,8 +122,13 @@ 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) {
|
||||||
|
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,21 +144,35 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
|
||||||
return req, nil
|
return req, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,6 +187,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
|
serverErr := make(chan error, 1)
|
||||||
|
parentDone := parentProcessDone(*parentPID)
|
||||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||||
if *exePath != "" && exists(*exePath) {
|
if *exePath != "" && exists(*exePath) {
|
||||||
go func() {
|
go func() {
|
||||||
@@ -136,21 +203,27 @@ 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).
|
||||||
Msg("Proxy started")
|
Msg("Proxy started")
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
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")
|
||||||
|
case <-parentDone:
|
||||||
|
zlog.Info().Int("ParentPID", *parentPID).Msg("Parent process exited")
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
zlog.Error().Err(err).Msg("Server shutdown error")
|
zlog.Error().Err(err).Msg("Server shutdown error")
|
||||||
}
|
}
|
||||||
setProxy(false, "", "")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parentProcessDone(pid int) <-chan struct{} {
|
||||||
|
if pid <= 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if !processExists(pid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
func processExists(pid int) bool {
|
||||||
|
err := syscall.Kill(pid, 0)
|
||||||
|
return err == nil || err == syscall.EPERM
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
// +build !darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func parentProcessDone(pid int) <-chan struct{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
zlog "github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startProxyRefreshLoop(host string, port string) func() {
|
||||||
|
done := make(chan struct{})
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(stopped)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := setProxy(true, host, port); err != nil {
|
||||||
|
zlog.Error().Err(err).Msg("Failed to refresh macOS proxy services")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
once.Do(func() {
|
||||||
|
close(done)
|
||||||
|
<-stopped
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
// +build !darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func startProxyRefreshLoop(host string, port string) func() {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
+31
-7
@@ -5,16 +5,40 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runWithAdmin(exePath string, env []string) error {
|
func runWithAdmin(exePath string, env []string) error {
|
||||||
escaped := strings.ReplaceAll(exePath, `"`, `\"`)
|
command := shellQuote(exePath)
|
||||||
script := fmt.Sprintf(`do shell script "%s" with administrator privileges`, escaped)
|
if len(env) > 0 {
|
||||||
|
command = strings.Join(shellEnvAssignments(env), " ") + " " + command
|
||||||
cmd := exec.Command("osascript", "-e", script)
|
}
|
||||||
cmd.Env = append(os.Environ(), env...)
|
command += " >/dev/null 2>&1 &"
|
||||||
return cmd.Start()
|
|
||||||
|
script := fmt.Sprintf("do shell script %s with administrator privileges", appleScriptString(command))
|
||||||
|
cmd := exec.Command("osascript", "-e", script)
|
||||||
|
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 + `"`
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-11
@@ -1,13 +1,4 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## First release
|
### UPDATE
|
||||||
|
- Fix bug in macos
|
||||||
### Added
|
|
||||||
- Initial release of Firefly Go Proxy
|
|
||||||
- Basic HTTP/HTTPS proxy functionality
|
|
||||||
- Domain-based request redirection
|
|
||||||
- URL pattern blocking
|
|
||||||
- Cross-platform support (Windows, macOS, Linux)
|
|
||||||
- System proxy configuration for all platforms
|
|
||||||
- Automatic certificate management
|
|
||||||
- Support for running executables with admin privileges
|
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tag": "1.0-01",
|
"tag": "1.2-01",
|
||||||
"title": "PreBuild Version 1.0 - 01"
|
"title": "PreBuild Version 1.2 - 01"
|
||||||
}
|
}
|
||||||
|
|
||||||
+446
-46
@@ -4,65 +4,465 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseNetworkServices(out string) []string {
|
type darwinProxyEndpoint struct {
|
||||||
lines := strings.Split(out, "\n")
|
enabled bool
|
||||||
var result []string
|
server string
|
||||||
|
port 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(arr []string, v string) bool {
|
type darwinProxySettings struct {
|
||||||
for _, x := range arr {
|
web darwinProxyEndpoint
|
||||||
if x == v {
|
secure darwinProxyEndpoint
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setProxy(enable bool, host string, port string) error {
|
var darwinProxyState = struct {
|
||||||
|
sync.Mutex
|
||||||
|
captured bool
|
||||||
|
previous map[string]darwinProxySettings
|
||||||
|
applied map[string]struct{}
|
||||||
|
}{}
|
||||||
|
|
||||||
|
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 proxyNetworkServices() ([]string, error) {
|
||||||
|
service, defaultErr := defaultNetworkService()
|
||||||
|
if defaultErr == nil && service != "" {
|
||||||
|
return []string{service}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
services, activeErr := activeNetworkServices()
|
||||||
|
if activeErr == nil && len(services) > 0 {
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultErr != nil || activeErr != nil {
|
||||||
|
return nil, errors.Join(defaultErr, activeErr)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no active network services found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultNetworkService() (string, error) {
|
||||||
|
device, err := defaultNetworkDevice()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesByDevice, err := networkServicesByDevice()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
service := servicesByDevice[device]
|
||||||
|
if service == "" {
|
||||||
|
return "", fmt.Errorf("network service for default device %s not found", device)
|
||||||
|
}
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultNetworkDevice() (string, error) {
|
||||||
|
out, err := exec.Command("route", "-n", "get", "default").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return "", formatCommandError("get default network route", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
key, value, ok := strings.Cut(line, ":")
|
||||||
|
if ok && strings.TrimSpace(key) == "interface" {
|
||||||
|
device := strings.TrimSpace(value)
|
||||||
|
if device != "" {
|
||||||
|
return device, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("default network interface not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func networkServicesByDevice() (map[string]string, error) {
|
||||||
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
|
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, formatCommandError("list network service order", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
services := make(map[string]string)
|
||||||
|
currentService := ""
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if service, ok := parseNetworkServiceOrderName(line); ok {
|
||||||
|
currentService = service
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if currentService == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if device, ok := parseNetworkServiceOrderDevice(line); ok {
|
||||||
|
services[device] = currentService
|
||||||
|
currentService = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(services) == 0 {
|
||||||
|
return nil, fmt.Errorf("no network service devices found")
|
||||||
|
}
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNetworkServiceOrderName(line string) (string, bool) {
|
||||||
|
if !strings.HasPrefix(line, "(") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
end := strings.Index(line, ")")
|
||||||
|
if end <= 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := strconv.Atoi(line[1:end]); err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
service := strings.TrimSpace(line[end+1:])
|
||||||
|
return service, service != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNetworkServiceOrderDevice(line string) (string, bool) {
|
||||||
|
_, value, ok := strings.Cut(line, "Device:")
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if idx := strings.IndexAny(value, ",)"); idx != -1 {
|
||||||
|
value = value[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
device := strings.TrimSpace(value)
|
||||||
|
return device, device != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeNetworkServices() ([]string, error) {
|
||||||
|
services, err := enabledNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
active := make([]string, 0, len(services))
|
||||||
|
for _, service := range services {
|
||||||
|
ok, err := isNetworkServiceActive(service)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
active = append(active, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(active) == 0 {
|
||||||
|
return nil, fmt.Errorf("no active network services found")
|
||||||
|
}
|
||||||
|
return active, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetworkServiceActive(service string) (bool, error) {
|
||||||
|
out, err := exec.Command("networksetup", "-getinfo", service).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false, formatCommandError("get network service info "+service, err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
key, value, ok := strings.Cut(line, ":")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key != "IP address" && key != "IPv6 IP address" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value != "" && !strings.EqualFold(value, "none") {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, 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 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 captureMissingProxySettings(services []string) error {
|
||||||
|
missing := make([]string, 0, len(services))
|
||||||
|
for _, service := range services {
|
||||||
|
if _, ok := darwinProxyState.previous[service]; !ok {
|
||||||
|
missing = append(missing, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := captureProxySettings(missing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for service, setting := range settings {
|
||||||
services := parseNetworkServices(string(out))
|
darwinProxyState.previous[service] = setting
|
||||||
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]
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serviceSet(services []string) map[string]struct{} {
|
||||||
|
set := make(map[string]struct{}, len(services))
|
||||||
|
for _, service := range services {
|
||||||
|
set[service] = struct{}{}
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
|
func appliedServicesNotIn(selected map[string]struct{}) []string {
|
||||||
|
services := make([]string, 0, len(darwinProxyState.applied))
|
||||||
|
for service := range darwinProxyState.applied {
|
||||||
|
if _, ok := selected[service]; !ok {
|
||||||
|
services = append(services, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return services
|
||||||
|
}
|
||||||
|
|
||||||
|
func appliedServices() []string {
|
||||||
|
services := make([]string, 0, len(darwinProxyState.applied))
|
||||||
|
for service := range darwinProxyState.applied {
|
||||||
|
services = append(services, service)
|
||||||
|
}
|
||||||
|
return services
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxySettingsForServices(settings map[string]darwinProxySettings, services []string) map[string]darwinProxySettings {
|
||||||
|
filtered := make(map[string]darwinProxySettings, len(services))
|
||||||
|
for _, service := range services {
|
||||||
|
if setting, ok := settings[service]; ok {
|
||||||
|
filtered[service] = setting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetDarwinProxyState() {
|
||||||
|
darwinProxyState.previous = nil
|
||||||
|
darwinProxyState.applied = nil
|
||||||
|
darwinProxyState.captured = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreAppliedProxySettings() error {
|
||||||
|
if len(darwinProxyState.applied) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := restoreProxySettings(proxySettingsForServices(
|
||||||
|
darwinProxyState.previous,
|
||||||
|
appliedServices(),
|
||||||
|
))
|
||||||
|
if err == nil {
|
||||||
|
darwinProxyState.applied = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setProxy(enable bool, host string, port string) error {
|
||||||
|
darwinProxyState.Lock()
|
||||||
|
defer darwinProxyState.Unlock()
|
||||||
|
|
||||||
|
if enable {
|
||||||
|
if host == "" || port == "" {
|
||||||
|
return fmt.Errorf("host and port are required to enable proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := proxyNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Join(err, restoreAppliedProxySettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
if darwinProxyState.previous == nil {
|
||||||
|
darwinProxyState.previous = make(map[string]darwinProxySettings)
|
||||||
|
}
|
||||||
|
if err := captureMissingProxySettings(services); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
darwinProxyState.captured = true
|
||||||
|
|
||||||
|
if err := setProxyForServices(services, host, port); err != nil {
|
||||||
|
restoreErr := restoreProxySettings(darwinProxyState.previous)
|
||||||
|
resetDarwinProxyState()
|
||||||
|
return errors.Join(err, restoreErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := serviceSet(services)
|
||||||
|
removed := appliedServicesNotIn(selected)
|
||||||
|
restoreErr := restoreProxySettings(proxySettingsForServices(darwinProxyState.previous, removed))
|
||||||
|
if restoreErr != nil {
|
||||||
|
for _, service := range removed {
|
||||||
|
selected[service] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
darwinProxyState.applied = selected
|
||||||
|
return restoreErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if darwinProxyState.captured {
|
||||||
|
err := restoreProxySettings(darwinProxyState.previous)
|
||||||
|
if err == nil {
|
||||||
|
resetDarwinProxyState()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := enabledNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return disableProxyForServices(services)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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