UPDATE: Enhance macOS build and proxy management functionality
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
.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
|
||||||
@@ -11,4 +30,4 @@ build_ico:
|
|||||||
set_logo:
|
set_logo:
|
||||||
@echo Embedding application icon...
|
@echo Embedding application icon...
|
||||||
go-winres simply --icon ./logo.ico
|
go-winres simply --icon ./logo.ico
|
||||||
@echo Done!
|
@echo Done!
|
||||||
|
|||||||
+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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,18 +39,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
proxyAddr := "127.0.0.1"
|
proxyAddr := "127.0.0.1"
|
||||||
|
proxyEnabled := false
|
||||||
|
|
||||||
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, "", "")
|
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
|
||||||
|
|
||||||
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) {
|
||||||
@@ -122,6 +130,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop := make(chan os.Signal, 1)
|
stop := make(chan os.Signal, 1)
|
||||||
|
serverErr := make(chan error, 1)
|
||||||
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() {
|
||||||
@@ -142,15 +151,19 @@ func main() {
|
|||||||
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")
|
||||||
|
}
|
||||||
|
|
||||||
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, "", "")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-5
@@ -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 + `"`
|
||||||
}
|
}
|
||||||
|
|||||||
+184
-41
@@ -4,65 +4,208 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"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
|
}
|
||||||
|
|
||||||
|
var darwinProxyState = struct {
|
||||||
|
sync.Mutex
|
||||||
|
captured bool
|
||||||
|
previous map[string]darwinProxySettings
|
||||||
|
}{}
|
||||||
|
|
||||||
|
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 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 false
|
|
||||||
|
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 setProxy(enable bool, host string, port string) error {
|
func setProxy(enable bool, host string, port string) error {
|
||||||
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
|
services, err := enabledNetworkServices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
services := parseNetworkServices(string(out))
|
darwinProxyState.Lock()
|
||||||
active := ""
|
defer darwinProxyState.Unlock()
|
||||||
|
|
||||||
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 {
|
if enable {
|
||||||
exec.Command("networksetup", "-setwebproxy", active, host, port).Run()
|
if host == "" || port == "" {
|
||||||
exec.Command("networksetup", "-setsecurewebproxy", active, host, port).Run()
|
return fmt.Errorf("host and port are required to enable proxy")
|
||||||
exec.Command("networksetup", "-setwebproxystate", active, "on").Run()
|
}
|
||||||
exec.Command("networksetup", "-setsecurewebproxystate", active, "on").Run()
|
|
||||||
} else {
|
if !darwinProxyState.captured {
|
||||||
exec.Command("networksetup", "-setwebproxystate", active, "off").Run()
|
settings, err := captureProxySettings(services)
|
||||||
exec.Command("networksetup", "-setsecurewebproxystate", active, "off").Run()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
darwinProxyState.previous = settings
|
||||||
|
darwinProxyState.captured = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setProxyForServices(services, host, port); err != nil {
|
||||||
|
restoreErr := restoreProxySettings(darwinProxyState.previous)
|
||||||
|
darwinProxyState.previous = nil
|
||||||
|
darwinProxyState.captured = false
|
||||||
|
return errors.Join(err, restoreErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if darwinProxyState.captured {
|
||||||
|
err := restoreProxySettings(darwinProxyState.previous)
|
||||||
|
if err == nil {
|
||||||
|
darwinProxyState.previous = nil
|
||||||
|
darwinProxyState.captured = false
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return disableProxyForServices(services)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user