7 Commits

Author SHA1 Message Date
Kain344 7bcd8b43d9 UPDATE: Add '-no-sys' flag to skip certificate installation and system proxy setup; update README with usage examples; increment release version to 1.2-02
Build and Release / release (push) Successful in 43s
2026-05-26 14:27:36 +07:00
Kain344 77d5a09021 UPDATE: Consolidate release upload steps for Windows and macOS in build workflow
Build and Release / release (push) Successful in 41s
2026-05-24 10:25:05 +07:00
Kain344 321c462f92 fix cicd
Build and Release / release (push) Failing after 41s
2026-05-24 10:22:47 +07:00
Kain344 d3ac27aa5d UPDATE: Enhance macOS support with new build steps, add admin relaunch functionality, and improve proxy management
Build and Release / release (push) Failing after 41s
2026-05-24 10:18:56 +07:00
Kain344 1abf0caee4 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
2026-05-23 19:53:13 +07:00
Kain344 1692bd73a2 UPDATE: New data
Build and Release / release (push) Successful in 19s
2026-05-16 09:25:03 +07:00
Kain344 211912d44b UPDATE: Enhance macOS build and proxy management functionality 2026-05-16 09:23:36 +07:00
19 changed files with 946 additions and 71 deletions
+17 -3
View File
@@ -20,6 +20,18 @@ 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
@@ -27,6 +39,8 @@ jobs:
- name: Upload release - name: Upload 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/FireflyGo_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,firefly-go-proxy-macos-amd64,firefly-go-proxy-macos-arm64"
+2
View File
@@ -1,6 +1,8 @@
.history .history
.vscode .vscode
*.exe *.exe
firefly-go-proxy-darwin-*
*.pem *.pem
*.log *.log
*.crt *.crt
+19
View File
@@ -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
+17
View File
@@ -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,7 +39,9 @@ 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
- `-no-sys`: Run only the proxy server; skip certificate installation, system proxy setup, and macOS/Linux admin relaunch
### Examples ### Examples
@@ -66,6 +69,20 @@ 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.
6. Start only the proxy server without changing system settings:
```bash
./firefly-proxy -no-sys -p 8888 //linux|macos
./firefly-proxy.exe -no-sys -p 8888 //windows
```
## How it works ## How it works
The proxy intercepts HTTP/HTTPS traffic and can: The proxy intercepts HTTP/HTTPS traffic and can:
+77
View File
@@ -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
}
+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
}
+5 -1
View File
@@ -10,7 +10,11 @@ import (
const caCertName = "firefly-go-proxy-ca.crt" const caCertName = "firefly-go-proxy-ca.crt"
func setupCertificate() (*tls.Certificate, error) { func setupCertificate(installSystemCA bool) (*tls.Certificate, error) {
if !installSystemCA {
return &goproxy.GoproxyCa, nil
}
if _, err := os.Stat(caCertName); os.IsNotExist(err) { if _, err := os.Stat(caCertName); os.IsNotExist(err) {
if err := os.WriteFile(caCertName, goproxy.GoproxyCa.Certificate[0], 0644); err != nil { if err := os.WriteFile(caCertName, goproxy.GoproxyCa.Certificate[0], 0644); err != nil {
return nil, err return nil, err
+60 -3
View File
@@ -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
}
}
+90 -13
View File
@@ -19,38 +19,88 @@ 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")
noSys := flag.Bool("no-sys", false, "skip certificate installation and system proxy setup")
flag.Parse() flag.Parse()
if !*noSys {
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
} }
cert, err := setupCertificate() cert, err := setupCertificate(!*noSys)
if err != nil { if err != nil {
zlog.Error().Err(err).Msg("Failed setup certificate") zlog.Error().Err(err).Msg("Failed setup certificate")
return return
} }
addr := ":" + port addr := ":" + port
proxyAddr := "127.0.0.1" proxyAddr := "127.0.0.1"
proxyEndpoint := proxyAddr + ":" + port
defer func() { defer func() {
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, "", "")
} }
}() }()
setProxy(true, proxyAddr, port) if !*noSys {
if err := setProxy(true, proxyAddr, port); err != nil {
zlog.Error().Err(err).Msg("Failed to set system proxy")
return
}
stopProxyRefresh := startProxyRefreshLoop(proxyAddr, port)
defer func() {
stopProxyRefresh()
if err := setProxy(false, "", ""); err != nil {
zlog.Error().Err(err).Msg("Failed to reset system proxy")
}
}()
} else {
zlog.Info().Msg("System certificate and proxy setup skipped")
}
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 +126,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 +148,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 +191,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 +207,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, "", "")
} }
+36
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin
// +build !darwin
package main
func parentProcessDone(pid int) <-chan struct{} {
return nil
}
+42
View File
@@ -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
})
}
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin
// +build !darwin
package main
func startProxyRefreshLoop(host string, port string) func() {
return func() {}
}
+29 -5
View File
@@ -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
}
command += " >/dev/null 2>&1 &"
script := fmt.Sprintf("do shell script %s with administrator privileges", appleScriptString(command))
cmd := exec.Command("osascript", "-e", script) cmd := exec.Command("osascript", "-e", script)
cmd.Env = append(os.Environ(), env...) return cmd.Run()
return cmd.Start() }
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 + `"`
} }
+1 -1
View File
@@ -1,4 +1,4 @@
# Changelog # Changelog
### UPDATE ### UPDATE
- Go 1.26.1, latest lib - Fix bug in macos
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"tag": "1.1-01", "tag": "1.2-02",
"title": "PreBuild Version 1.1 - 01" "title": "PreBuild Version 1.2 - 02"
} }
+441 -41
View File
@@ -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 { type darwinProxySettings struct {
if strings.Contains(line, "(Hardware Port:") { web darwinProxyEndpoint
start := strings.Index(line, "Hardware Port: ") + len("Hardware Port: ") secure darwinProxyEndpoint
end := strings.Index(line[start:], ",") }
if end > 0 {
result = append(result, line[start:start+end]) 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 result
return "", fmt.Errorf("default network interface not found")
} }
func contains(arr []string, v string) bool { func networkServicesByDevice() (map[string]string, error) {
for _, x := range arr { out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
if x == v { if err != nil {
return true 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 = ""
} }
} }
return false
if len(services) == 0 {
return nil, fmt.Errorf("no network service devices found")
}
return services, nil
} }
func setProxy(enable bool, host string, port string) error { func parseNetworkServiceOrderName(line string) (string, bool) {
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput() 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)
}
+9
View File
@@ -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]