4 Commits

Author SHA1 Message Date
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
Kain344 24b28cdcca UPDATE: FIx cicd
Build and Release / release (push) Successful in 18s
2026-04-05 12:34:16 +07:00
Kain344 50d489dabe UPDATE: go 1.26.1, new lib
Build and Release / release (push) Failing after 36s
2026-04-05 12:23:32 +07:00
11 changed files with 341 additions and 99 deletions
+1 -1
View File
@@ -27,6 +27,6 @@ jobs:
- name: Upload 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/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"
+2
View File
@@ -1,6 +1,8 @@
.history
.vscode
*.exe
firefly-go-proxy-darwin-*
*.pem
*.log
*.crt
+19
View File
@@ -1,8 +1,27 @@
.PHONY: build build_mac build_mac_amd64 build_mac_arm64 build_ico set_logo
build:
@echo Building windows binary...
set GOOS=windows&& set GOARCH=amd64&& set CGO_ENABLED=0&& go build -trimpath -ldflags="-s -w" .
@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:
@echo Building application icon...
magick logo.jpg -define icon:auto-resize=256,128,64,48,32,16 ./logo.ico
+60 -3
View File
@@ -4,22 +4,79 @@
package main
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"os/exec"
)
const darwinSystemKeychain = "/Library/Keychains/System.keychain"
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(
"security",
"add-trusted-cert",
"-d",
"-r", "trustRoot",
"-k", "/Library/Keychains/System.keychain",
"-k", darwinSystemKeychain,
absPath,
)
if err := cmd.Run(); err != nil {
return err
if out, err := cmd.CombinedOutput(); err != nil {
return formatCommandError("install CA into macOS system keychain", err, out)
}
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
}
}
+6 -6
View File
@@ -1,16 +1,16 @@
module firefly-go-proxy
go 1.25.5
go 1.26.1
require (
github.com/elazarl/goproxy v1.7.2
github.com/rs/zerolog v1.34.0
golang.org/x/sys v0.39.0
github.com/elazarl/goproxy v1.8.3
github.com/rs/zerolog v1.35.0
golang.org/x/sys v0.42.0
)
require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/text v0.35.0 // indirect
)
+14 -21
View File
@@ -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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
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/elazarl/goproxy v1.8.3 h1:XhiZpzW0NvsGOqSv/F3v4+1F29842yYaJNN+In5Fnuc=
github.com/elazarl/goproxy v1.8.3/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E=
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-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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
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=
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+20 -7
View File
@@ -39,18 +39,26 @@ func main() {
}
addr := ":" + port
proxyAddr := "127.0.0.1"
proxyEnabled := false
defer func() {
if r := recover(); r != nil {
zlog.Error().
Interface("panic", r).
Msg("Unexpected panic, resetting system proxy")
setProxy(false, "", "")
Msg("Unexpected panic")
}
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)}
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)
serverErr := make(chan error, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
if *exePath != "" && exists(*exePath) {
go func() {
@@ -142,15 +151,19 @@ func main() {
Str("ExePath", *exePath).
Msg("Proxy started")
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)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
zlog.Error().Err(err).Msg("Server shutdown error")
}
setProxy(false, "", "")
}
+29 -5
View File
@@ -5,16 +5,40 @@ package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
func runWithAdmin(exePath string, env []string) error {
escaped := strings.ReplaceAll(exePath, `"`, `\"`)
script := fmt.Sprintf(`do shell script "%s" with administrator privileges`, escaped)
command := shellQuote(exePath)
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.Env = append(os.Environ(), env...)
return cmd.Start()
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
View File
@@ -1,13 +1,4 @@
# Changelog
## First release
### 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
### UPDATE
- Go 1.26.1, latest lib
+2 -2
View File
@@ -1,5 +1,5 @@
{
"tag": "1.0-01",
"title": "PreBuild Version 1.0 - 01"
"tag": "1.1-02",
"title": "PreBuild Version 1.1 - 02"
}
+184 -41
View File
@@ -4,65 +4,208 @@
package main
import (
"errors"
"fmt"
"os/exec"
"strings"
"sync"
)
func parseNetworkServices(out string) []string {
lines := strings.Split(out, "\n")
var result []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
type darwinProxyEndpoint struct {
enabled bool
server string
port string
}
func contains(arr []string, v string) bool {
for _, x := range arr {
if x == v {
return true
type darwinProxySettings struct {
web darwinProxyEndpoint
secure darwinProxyEndpoint
}
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 {
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
services, err := enabledNetworkServices()
if err != nil {
return err
}
services := parseNetworkServices(string(out))
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]
}
darwinProxyState.Lock()
defer darwinProxyState.Unlock()
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()
if host == "" || port == "" {
return fmt.Errorf("host and port are required to enable proxy")
}
if !darwinProxyState.captured {
settings, err := captureProxySettings(services)
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)
}