Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cdd50f230 | |||
| 7bcd8b43d9 | |||
| 77d5a09021 | |||
| 321c462f92 | |||
| d3ac27aa5d |
@@ -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"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ go build
|
|||||||
- `-b`: Comma-separated list of blocked ports
|
- `-b`: Comma-separated list of blocked ports
|
||||||
- `-p`: Proxy listen port (default: auto)
|
- `-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
|
||||||
|
|
||||||
@@ -76,6 +77,12 @@ go build
|
|||||||
|
|
||||||
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.
|
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:
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
func relaunchWithAdminIfNeeded() (bool, error) {
|
func relaunchWithAdminIfNeeded() (bool, error) {
|
||||||
@@ -25,11 +28,17 @@ func relaunchWithAdminIfNeeded() (bool, error) {
|
|||||||
return false, fmt.Errorf("get working directory: %w", err)
|
return false, fmt.Errorf("get working directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wrapperPID := os.Getpid()
|
||||||
|
launcherPID := os.Getppid()
|
||||||
|
|
||||||
args := make([]string, 0, len(os.Args))
|
args := make([]string, 0, len(os.Args))
|
||||||
args = append(args, shellQuote(exePath))
|
args = append(args, shellQuote(exePath))
|
||||||
for _, arg := range os.Args[1:] {
|
for _, arg := range os.Args[1:] {
|
||||||
args = append(args, shellQuote(arg))
|
args = append(args, shellQuote(arg))
|
||||||
}
|
}
|
||||||
|
if !hasFlagArg("parent-pid") {
|
||||||
|
args = append(args, shellQuote("-parent-pid"), shellQuote(strconv.Itoa(wrapperPID)))
|
||||||
|
}
|
||||||
|
|
||||||
command := fmt.Sprintf(
|
command := fmt.Sprintf(
|
||||||
"cd %s && %s > /dev/null 2>&1 &",
|
"cd %s && %s > /dev/null 2>&1 &",
|
||||||
@@ -42,5 +51,27 @@ func relaunchWithAdminIfNeeded() (bool, error) {
|
|||||||
return false, formatCommandError("relaunch proxy as admin", err, out)
|
return false, formatCommandError("relaunch proxy as admin", err, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitForRelaunchedProxyShutdown(launcherPID)
|
||||||
return true, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ 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 _, 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !installSystemCA {
|
||||||
|
return &goproxy.GoproxyCa, nil
|
||||||
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(caCertName)
|
absPath, err := filepath.Abs(caCertName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -37,16 +37,20 @@ func main() {
|
|||||||
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)")
|
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()
|
||||||
|
|
||||||
relaunched, err := relaunchWithAdminIfNeeded()
|
if !*noSys {
|
||||||
if err != nil {
|
relaunched, err := relaunchWithAdminIfNeeded()
|
||||||
zlog.Error().Err(err).Msg("Failed to relaunch with admin privileges")
|
if err != nil {
|
||||||
return
|
zlog.Error().Err(err).Msg("Failed to relaunch with admin privileges")
|
||||||
}
|
return
|
||||||
if relaunched {
|
}
|
||||||
zlog.Info().Msg("Relaunched with admin privileges")
|
if relaunched {
|
||||||
return
|
zlog.Info().Msg("Relaunched with admin privileges")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockedPorts := parseBlockedPorts(*blockedStr)
|
blockedPorts := parseBlockedPorts(*blockedStr)
|
||||||
@@ -65,7 +69,7 @@ func main() {
|
|||||||
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
|
||||||
@@ -73,7 +77,6 @@ func main() {
|
|||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
proxyAddr := "127.0.0.1"
|
proxyAddr := "127.0.0.1"
|
||||||
proxyEndpoint := proxyAddr + ":" + port
|
proxyEndpoint := proxyAddr + ":" + port
|
||||||
proxyEnabled := false
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -81,18 +84,23 @@ func main() {
|
|||||||
Interface("panic", r).
|
Interface("panic", r).
|
||||||
Msg("Unexpected panic")
|
Msg("Unexpected panic")
|
||||||
}
|
}
|
||||||
if proxyEnabled {
|
}()
|
||||||
|
|
||||||
|
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 {
|
if err := setProxy(false, "", ""); err != nil {
|
||||||
zlog.Error().Err(err).Msg("Failed to reset system proxy")
|
zlog.Error().Err(err).Msg("Failed to reset system proxy")
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
}()
|
} else {
|
||||||
|
zlog.Info().Msg("System certificate and proxy setup skipped")
|
||||||
if err := setProxy(true, proxyAddr, port); err != nil {
|
|
||||||
zlog.Error().Err(err).Msg("Failed to set system proxy")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
proxyEnabled = true
|
|
||||||
|
|
||||||
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) {
|
||||||
@@ -124,6 +132,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matchDomain(host, AlwaysIgnoreDomains) {
|
if matchDomain(host, AlwaysIgnoreDomains) {
|
||||||
|
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +176,7 @@ func main() {
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
|
||||||
return req, nil
|
return req, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -182,6 +192,7 @@ func main() {
|
|||||||
|
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
serverErr := make(chan error, 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() {
|
||||||
@@ -210,6 +221,8 @@ func main() {
|
|||||||
case <-stop:
|
case <-stop:
|
||||||
case err := <-serverErr:
|
case err := <-serverErr:
|
||||||
zlog.Error().Err(err).Msg("ListenAndServe failed")
|
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)
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
### UPDATE
|
### UPDATE
|
||||||
- Go 1.26.1, latest lib
|
- Fix bug in macos
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tag": "1.1-03",
|
"tag": "1.2-03",
|
||||||
"title": "PreBuild Version 1.1 - 03"
|
"title": "PreBuild Version 1.2 - 03"
|
||||||
}
|
}
|
||||||
|
|
||||||
+271
-14
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -26,6 +27,7 @@ var darwinProxyState = struct {
|
|||||||
sync.Mutex
|
sync.Mutex
|
||||||
captured bool
|
captured bool
|
||||||
previous map[string]darwinProxySettings
|
previous map[string]darwinProxySettings
|
||||||
|
applied map[string]struct{}
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
func enabledNetworkServices() ([]string, error) {
|
func enabledNetworkServices() ([]string, error) {
|
||||||
@@ -50,6 +52,171 @@ func enabledNetworkServices() ([]string, error) {
|
|||||||
return services, nil
|
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()
|
||||||
|
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) {
|
func getProxySettings(service string) (darwinProxySettings, error) {
|
||||||
web, webErr := getProxyEndpoint("-getwebproxy", service)
|
web, webErr := getProxyEndpoint("-getwebproxy", service)
|
||||||
secure, secureErr := getProxyEndpoint("-getsecurewebproxy", service)
|
secure, secureErr := getProxyEndpoint("-getsecurewebproxy", service)
|
||||||
@@ -166,12 +333,86 @@ func captureProxySettings(services []string) (map[string]darwinProxySettings, er
|
|||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setProxy(enable bool, host string, port string) error {
|
func captureMissingProxySettings(services []string) error {
|
||||||
services, err := enabledNetworkServices()
|
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 {
|
||||||
|
darwinProxyState.previous[service] = setting
|
||||||
|
}
|
||||||
|
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()
|
darwinProxyState.Lock()
|
||||||
defer darwinProxyState.Unlock()
|
defer darwinProxyState.Unlock()
|
||||||
|
|
||||||
@@ -180,32 +421,48 @@ func setProxy(enable bool, host string, port string) error {
|
|||||||
return fmt.Errorf("host and port are required to enable proxy")
|
return fmt.Errorf("host and port are required to enable proxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !darwinProxyState.captured {
|
services, err := proxyNetworkServices()
|
||||||
settings, err := captureProxySettings(services)
|
if err != nil {
|
||||||
if err != nil {
|
return errors.Join(err, restoreAppliedProxySettings())
|
||||||
return err
|
|
||||||
}
|
|
||||||
darwinProxyState.previous = settings
|
|
||||||
darwinProxyState.captured = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
if err := setProxyForServices(services, host, port); err != nil {
|
||||||
restoreErr := restoreProxySettings(darwinProxyState.previous)
|
restoreErr := restoreProxySettings(darwinProxyState.previous)
|
||||||
darwinProxyState.previous = nil
|
resetDarwinProxyState()
|
||||||
darwinProxyState.captured = false
|
|
||||||
return errors.Join(err, restoreErr)
|
return errors.Join(err, restoreErr)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
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 {
|
if darwinProxyState.captured {
|
||||||
err := restoreProxySettings(darwinProxyState.previous)
|
err := restoreProxySettings(darwinProxyState.previous)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
darwinProxyState.previous = nil
|
resetDarwinProxyState()
|
||||||
darwinProxyState.captured = false
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services, err := enabledNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return disableProxyForServices(services)
|
return disableProxyForServices(services)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user