commit 9d769ed08c82678a19ee21639fe92ed285dac1bd Author: AzenKain Date: Fri Dec 12 17:37:34 2025 +0700 init diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..6292ca2 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build and Release +run-name: ${{ gitea.actor }} build 🚀 + +on: + push: + branches: + - master +jobs: + release: + runs-on: ubuntu-latest + container: + image: azenkain/go-node:latest + + steps: + - uses: actions/checkout@v4 + + - name: Download Go dependencies + run: go mod download + + - name: Build for Windows + run: GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" . + + - name: Grant execute permissions + run: | + chmod +x ./script/release-uploader + + - 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" + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07d8d9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.history +.vscode +*.exe +*.pem +*.log +*.crt \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..964aa69 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 Firefly-Shelter + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..84b52cc --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +build: + @echo Building windows binary... + set GOOS=windows&& set GOARCH=amd64&& set CGO_ENABLED=0&& go build -trimpath -ldflags="-s -w" . + @echo Done! + +build_ico: + @echo Building application icon... + magick logo.jpg -define icon:auto-resize=256,128,64,48,32,16 ./logo.ico + @echo Done! + +set_logo: + @echo Embedding application icon... + go-winres simply --icon ./logo.ico + @echo Done! \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e41dac6 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Firefly Go Proxy + +A lightweight HTTP/HTTPS proxy server with domain redirection and request blocking capabilities. This tool is designed to help with local development and testing by intercepting and modifying HTTP/HTTPS traffic. + +## Features + +- HTTP/HTTPS proxy with MITM support +- Domain-based request redirection +- URL pattern blocking +- Automatic certificate management +- Cross-platform support (Windows, macOS, Linux) +- System proxy configuration + +## Installation + +### Prerequisites + +- Go 1.22 or higher +- Git + +### Building from source + +```bash +cd firefly-go-proxy +go build +``` + +## Usage + +### Basic usage + +```bash +./firefly-proxy [flags] //linux|macos +./firefly-proxy.exe [flags] //windows +``` + +### Available Flags + +- `-r`: Redirect target host (default: "127.0.0.1:21000") +- `-b`: Comma-separated list of blocked ports +- `-e`: Path to an executable to run with admin privileges + +### Examples + +1. Start proxy with default settings: + ```bash + ./firefly-proxy //linux|macos + ./firefly-proxy.exe //windows + ``` + +2. Redirect traffic to a different host: + ```bash + ./firefly-proxy -r 192.168.1.100:8080 //linux|macos + ./firefly-proxy.exe -r 192.168.1.100:8080 //windows + ``` + +3. Block specific ports: + ```bash + ./firefly-proxy -b "80,443,8080" //linux|macos + ./firefly-proxy.exe -b "80,443,8080" //windows + ``` + +4. Run an executable with admin privileges: + ```bash + ./firefly-proxy -e "/path/to/your/executable" //linux|macos + ./firefly-proxy.exe -e "/path/to/your/executable" //windows + ``` + +## How it works + +The proxy intercepts HTTP/HTTPS traffic and can: +- Redirect requests based on domain names +- Block specific URLs or patterns +- Handle SSL/TLS connections with custom CA certificates +- Automatically configure system proxy settings + +## License + +MIT License + + diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..4ad8ef9 --- /dev/null +++ b/cache.go @@ -0,0 +1,37 @@ +package main + +import ( + "crypto/tls" + "sync" +) + +type CertStorage struct { + certs map[string]*tls.Certificate + mtx sync.RWMutex +} + +func (cs *CertStorage) Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) { + cs.mtx.RLock() + cert, ok := cs.certs[hostname] + cs.mtx.RUnlock() + if ok { + return cert, nil + } + + cert, err := gen() + if err != nil { + return nil, err + } + + cs.mtx.Lock() + cs.certs[hostname] = cert + cs.mtx.Unlock() + + return cert, nil +} + +func NewCertStorage() *CertStorage { + return &CertStorage{ + certs: make(map[string]*tls.Certificate), + } +} \ No newline at end of file diff --git a/cert.go b/cert.go new file mode 100644 index 0000000..a66238f --- /dev/null +++ b/cert.go @@ -0,0 +1,29 @@ +package main + +import ( + "crypto/tls" + "os" + "path/filepath" + + "github.com/elazarl/goproxy" +) + +const caCertName = "firefly-go-proxy-ca.crt" + +func setupCertificate() (*tls.Certificate, error) { + if _, err := os.Stat(caCertName); os.IsNotExist(err) { + if err := os.WriteFile(caCertName, goproxy.GoproxyCa.Certificate[0], 0644); err != nil { + return nil, err + } + } + + absPath, err := filepath.Abs(caCertName) + if err != nil { + return nil, err + } + if err := installCA(absPath); err != nil { + return nil, err + } + + return &goproxy.GoproxyCa, nil +} diff --git a/cert_darwin.go b/cert_darwin.go new file mode 100644 index 0000000..7fd1eec --- /dev/null +++ b/cert_darwin.go @@ -0,0 +1,25 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "os/exec" +) + +func installCA(absPath string) error { + cmd := exec.Command( + "security", + "add-trusted-cert", + "-d", + "-r", "trustRoot", + "-k", "/Library/Keychains/System.keychain", + absPath, + ) + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/cert_linux.go b/cert_linux.go new file mode 100644 index 0000000..b83b5ea --- /dev/null +++ b/cert_linux.go @@ -0,0 +1,62 @@ +//go:build linux +// +build linux + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func installCA(absPath string) error { + // Detect distro + data, err := os.ReadFile("/etc/os-release") + if err != nil { + return fmt.Errorf("cannot detect distro: %v", err) + } + content := string(data) + + // Debian/Ubuntu/Kali + if strings.Contains(content, "ID=debian") || + strings.Contains(content, "ID=ubuntu") || + strings.Contains(content, "ID=kali") { + + destDir := "/usr/local/share/ca-certificates" + if err := os.MkdirAll(destDir, 0755); err != nil { + return fmt.Errorf("failed to create cert dir: %v", err) + } + + filename := filepath.Base(absPath) + destPath := filepath.Join(destDir, filename) + + inputData, err := os.ReadFile(absPath) + if err != nil { + return fmt.Errorf("failed to read source file: %v", err) + } + if err := os.WriteFile(destPath, inputData, 0644); err != nil { + return fmt.Errorf("failed to write cert file to system: %v", err) + } + + fmt.Printf("Updating certificates for Debian/Ubuntu...\n") + cmd := exec.Command("update-ca-certificates") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() + } + + // Arch / Manjaro + if strings.Contains(content, "ID=arch") || + strings.Contains(content, "ID=manjaro") { + + cmd := exec.Command("trust", "anchor", "--store", absPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + + return fmt.Errorf("unsupported Linux distribution") +} diff --git a/cert_other.go b/cert_other.go new file mode 100644 index 0000000..b1e8d1c --- /dev/null +++ b/cert_other.go @@ -0,0 +1,7 @@ +//go:build !windows && !darwin && !linux + +package main + +func installCA(certPath string) error { + return nil +} diff --git a/cert_windows.go b/cert_windows.go new file mode 100644 index 0000000..0fe6705 --- /dev/null +++ b/cert_windows.go @@ -0,0 +1,17 @@ +//go:build windows +// +build windows + +package main + +import ( + "os/exec" +) + +func installCA(absPath string) error { + cmd := exec.Command("certutil", "-addstore", "-user", "root", absPath) + if err := cmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..8d3eac4 --- /dev/null +++ b/config.go @@ -0,0 +1,62 @@ +package main + +var RedirectDomains = []string{ + ".hoyoverse.com", + ".mihoyo.com", + ".yuanshen.com", + ".bhsr.com", + ".starrails.com", + ".juequling.com", + ".zenlesszonezero.com", + ".bh3.com", + ".honkaiimpact3.com", + ".mob.com", + ".hyg.com", +} + +var AlwaysIgnoreDomains = []string{ + "autopatchcn.yuanshen.com", + "autopatchhk.yuanshen.com", + "autopatchcn.juequling.com", + "autopatchos.zenlesszonezero.com", + "autopatchcn.bhsr.com", + "autopatchos.starrails.com", +} + +var BlockUrls = []string{ + "/data_abtest_api/config/experiment/list", + "/common/hkrpg_global/announcement/api/getAlertPic", + "/common/hkrpg_global/announcement/api/getAlertAnn", + "/hkrpg_global/combo/red_dot/list", + "/sdk/upload", + "/sdk/dataUpload", + "/common/h5log/log/batch", + "/crash/dataUpload", + "/crashdump/dataUpload", + "/client/event/dataUpload", + "/log", + "/asm/dataUpload", + "/sophon/dataUpload", + "/apm/dataUpload", + "/2g/dataUpload", + "/v1/firelog/legacy/log", + "/h5/upload", + "/_ts", + "/perf/config/verify", + "/ptolemaios_api/api/reportStrategyData", + "/combo/box/api/config/sdk/combo", + "/hkrpg_global/combo/granter/api/compareProtocolVersion", + "/admin/mi18n", + "/combo/box/api/config/sw/precache", + "/hkrpg_global/mdk/agreement/api/getAgreementInfos", + "/device-fp/api/getExtList", + "/admin/mi18n/plat_os/m09291531181441/m09291531181441-version.json", + "/admin/mi18n/plat_oversea/m2020030410/m2020030410-version.json", +} + +var ForceRedirectOnUrlContains = []string{ + "/query_dispatch", + "/query_gateway", + "/query_region_list", + "/query_cur_region", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bb6338f --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module firefly-go-proxy + +go 1.25.4 + +require ( + github.com/elazarl/goproxy v1.7.2 + github.com/rs/zerolog v1.34.0 + golang.org/x/sys v0.39.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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..86e48f3 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/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= +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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/log.go b/log.go new file mode 100644 index 0000000..d6f034e --- /dev/null +++ b/log.go @@ -0,0 +1,17 @@ +package main + +import ( + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +func init() { + output := zerolog.ConsoleWriter{ + Out: os.Stdout, + PartsOrder: []string{"level", "message"}, + } + + log.Logger = zerolog.New(output).With().Logger() +} diff --git a/logo.ico b/logo.ico new file mode 100644 index 0000000..06ec4b7 Binary files /dev/null and b/logo.ico differ diff --git a/logo.jpg b/logo.jpg new file mode 100644 index 0000000..d4d605e Binary files /dev/null and b/logo.jpg differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..8402143 --- /dev/null +++ b/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/elazarl/goproxy" + zlog "github.com/rs/zerolog/log" +) + +var ENV_CONFIG = make([]string, 0) + +func main() { + redirectHost := flag.String("r", "127.0.0.1:21000", "redirect target host") + blockedStr := flag.String("b", "", "comma separated list of blocked ports") + exePath := flag.String("e", "", "path to the executable") + flag.Parse() + + blockedPorts := parseBlockedPorts(*blockedStr) + port := findFreePort(blockedPorts) + if port == "-1" { + zlog.Error().Str("port", port).Msg("No free port available") + return + } + + cert, err := setupCertificate() + if err != nil { + zlog.Error().Err(err).Msg("Failed setup certificate") + return + } + addr := ":" + port + proxyAddr := "127.0.0.1" + + defer func() { + if r := recover(); r != nil { + zlog.Error(). + Interface("panic", r). + Msg("Unexpected panic, resetting system proxy") + + setProxy(false, "", "") + } + }() + + setProxy(true, proxyAddr, port) + + customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)} + var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { + domain := cleanHost(host) + if matchDomain(domain, RedirectDomains) { + return customCaMitm, host + } + return goproxy.OkConnect, host + } + + proxy := goproxy.NewProxyHttpServer() + proxy.Logger = log.New(io.Discard, "", 0) + proxy.Tr = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + MaxIdleConns: 1000, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + DisableCompression: false, + } + proxy.CertStore = NewCertStorage() + proxy.OnRequest().HandleConnect(customAlwaysMitm) + + proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { + host := req.URL.Hostname() + path := req.URL.Path + + if matchDomain(host, AlwaysIgnoreDomains) { + return req, nil + } + + if matchDomain(host, RedirectDomains) { + if matchURL(path, BlockUrls) { + full := req.URL.String() + zlog.Warn().Str("url", full).Msg("Blocked URL") + return req, goproxy.NewResponse( + req, + goproxy.ContentTypeText, + http.StatusNotFound, + `{\n"message": "blocked by proxy",\n,"success": false,\n"retcode": -1\n}`, + ) + } + full := req.URL.String() + if matchURL(full, ForceRedirectOnUrlContains) { + + zlog.Info().Str("Url", full).Msg("Force redirect") + + req.URL.Scheme = "http" + req.URL.Host = *redirectHost + return req, nil + } + + zlog.Info().Str("Host", host).Msg("Redirect domain") + req.URL.Scheme = "http" + req.URL.Host = *redirectHost + return req, nil + } + + return req, nil + }) + + srv := &http.Server{ + Addr: addr, + Handler: proxy, + ReadTimeout: 10 * time.Second, + ReadHeaderTimeout: 5 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + if *exePath != "" && exists(*exePath) { + go func() { + time.Sleep(1 * time.Second) + err := runWithAdmin(*exePath, ENV_CONFIG) + if err != nil { + zlog.Error().Err(err).Msg("Failed to start exe as admin") + } else { + zlog.Info().Str("ExePath", *exePath).Msg("Started exe as admin") + } + }() + } + go func() { + zlog.Info(). + Str("ProxyAddress", proxyAddr). + Str("RedirectTo", *redirectHost). + Str("BlockedPorts", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(blockedPorts)), ","), "[]")). + Str("ExePath", *exePath). + Msg("Proxy started") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + zlog.Fatal().Err(err).Msg("ListenAndServe failed") + } + }() + + <-stop + 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, "", "") +} diff --git a/rsrc_windows_386.syso b/rsrc_windows_386.syso new file mode 100644 index 0000000..d34c4c9 Binary files /dev/null and b/rsrc_windows_386.syso differ diff --git a/rsrc_windows_amd64.syso b/rsrc_windows_amd64.syso new file mode 100644 index 0000000..a1457c6 Binary files /dev/null and b/rsrc_windows_amd64.syso differ diff --git a/run_admin.go b/run_admin.go new file mode 100644 index 0000000..3b4f03e --- /dev/null +++ b/run_admin.go @@ -0,0 +1,7 @@ +//go:build !windows && !darwin && !linux + +package main + +func runWithAdmin(exePath string, env []string) error { + return nil +} diff --git a/run_admin_darwin.go b/run_admin_darwin.go new file mode 100644 index 0000000..7da6101 --- /dev/null +++ b/run_admin_darwin.go @@ -0,0 +1,20 @@ +//go:build darwin +// +build darwin + +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) + + cmd := exec.Command("osascript", "-e", script) + cmd.Env = append(os.Environ(), env...) + return cmd.Start() +} diff --git a/run_admin_linux.go b/run_admin_linux.go new file mode 100644 index 0000000..9965641 --- /dev/null +++ b/run_admin_linux.go @@ -0,0 +1,15 @@ +//go:build linux +// +build linux + +package main + +import ( + "os" + "os/exec" +) + +func runWithAdmin(exePath string, env []string) error { + cmd := exec.Command("pkexec", exePath) + cmd.Env = append(os.Environ(), env...) + return cmd.Start() +} diff --git a/run_admin_windows.go b/run_admin_windows.go new file mode 100644 index 0000000..5cb3000 --- /dev/null +++ b/run_admin_windows.go @@ -0,0 +1,15 @@ +//go:build windows +// +build windows + +package main + +import ( + "os" + "os/exec" +) + +func runWithAdmin(exePath string, env []string) error { + cmd := exec.Command("powershell", "Start-Process", exePath, "-Verb", "runAs") + cmd.Env = append(os.Environ(), env...) + return cmd.Start() +} diff --git a/script/README_Note.md b/script/README_Note.md new file mode 100644 index 0000000..71dac1b --- /dev/null +++ b/script/README_Note.md @@ -0,0 +1,13 @@ +# 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 diff --git a/script/release-uploader b/script/release-uploader new file mode 100644 index 0000000..8fefe35 Binary files /dev/null and b/script/release-uploader differ diff --git a/script/release.json b/script/release.json new file mode 100644 index 0000000..127336b --- /dev/null +++ b/script/release.json @@ -0,0 +1,5 @@ +{ + "tag": "1.0-01", + "title": "PreBuild Version 1.0 - 01" +} + \ No newline at end of file diff --git a/system_proxy.go b/system_proxy.go new file mode 100644 index 0000000..0e84ed9 --- /dev/null +++ b/system_proxy.go @@ -0,0 +1,7 @@ +//go:build !windows && !darwin && !linux + +package main + +func setProxy(enable bool, host string, port string) error { + return nil +} \ No newline at end of file diff --git a/system_proxy_darwin.go b/system_proxy_darwin.go new file mode 100644 index 0000000..e4d70b3 --- /dev/null +++ b/system_proxy_darwin.go @@ -0,0 +1,68 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "os/exec" + "strings" +) + +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 +} + +func contains(arr []string, v string) bool { + for _, x := range arr { + if x == v { + return true + } + } + return false +} + +func setProxy(enable bool, host string, port string) error { + out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput() + 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] + } + + 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 +} diff --git a/system_proxy_linux.go b/system_proxy_linux.go new file mode 100644 index 0000000..f682a13 --- /dev/null +++ b/system_proxy_linux.go @@ -0,0 +1,24 @@ +//go:build linux +// +build linux + +package main + +import "fmt" + +func setProxy(enable bool, host string, port string) error { + + httpProxy1 := fmt.Sprintf("HTTP_PROXY=http://%s:%s", host, port) + httpProxy2 := fmt.Sprintf("http_proxy=http://%s:%s", host, port) + + ENV_CONFIG = append(ENV_CONFIG, httpProxy1, httpProxy2) + + httpsProxy1 := fmt.Sprintf("HTTPS_PROXY=http://%s:%s", host, port) + httpsProxy2 := fmt.Sprintf("https_proxy=http://%s:%s", host, port) + ENV_CONFIG = append(ENV_CONFIG, httpsProxy1, httpsProxy2) + + if enable { + ENV_CONFIG = make([]string, 0) + } + + return nil +} diff --git a/system_proxy_windows.go b/system_proxy_windows.go new file mode 100644 index 0000000..c90032f --- /dev/null +++ b/system_proxy_windows.go @@ -0,0 +1,43 @@ +//go:build windows +// +build windows + +package main + +import ( + "fmt" + "syscall" + + "golang.org/x/sys/windows/registry" +) + +func setProxy(enable bool, host string, port string) error { + k, _, err := registry.CreateKey( + registry.CURRENT_USER, + `Software\Microsoft\Windows\CurrentVersion\Internet Settings`, + registry.SET_VALUE, + ) + if err != nil { + return err + } + + if enable { + k.SetDWordValue("ProxyEnable", 1) + + addr := fmt.Sprintf("%s:%s", host, port) + val := fmt.Sprintf("http=%s;https=%s", addr, addr) + + k.SetStringValue("ProxyServer", val) + + } else { + k.SetDWordValue("ProxyEnable", 0) + } + + k.Close() + + d := syscall.NewLazyDLL("wininet.dll") + o := d.NewProc("InternetSetOptionW") + o.Call(0, 39, 0, 0) + o.Call(0, 37, 0, 0) + + return nil +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..870d8ef --- /dev/null +++ b/utils.go @@ -0,0 +1,67 @@ +package main + +import ( + "net" + "os" + "slices" + "strconv" + "strings" +) + +func matchDomain(host string, list []string) bool { + for _, d := range list { + if strings.HasSuffix(host, d) { + return true + } + } + return false +} + +func matchURL(url string, list []string) bool { + for _, u := range list { + if strings.HasPrefix(url, u) { + return true + } + } + return false +} + +func cleanHost(h string) string { + if idx := strings.LastIndex(h, ":"); idx != -1 { + return h[:idx] + } + return h +} + +func findFreePort(blocked []int) string { + for { + ln, err := net.Listen("tcp", ":0") + if err != nil { + return "-1" + } + port := ln.Addr().(*net.TCPAddr).Port + ln.Close() + + if !slices.Contains(blocked, port) { + return strconv.Itoa(port) + } + } +} + +func parseBlockedPorts(s string) []int { + var ports []int + if s == "" { + return ports + } + for _, p := range strings.Split(s, ",") { + if port, err := strconv.Atoi(strings.TrimSpace(p)); err == nil { + ports = append(ports, port) + } + } + return ports +} + +func exists(path string) bool { + _, err := os.Stat(path) + return err == nil +}