This commit is contained in:
32
.gitea/workflows/build.yml
Normal file
32
.gitea/workflows/build.yml
Normal file
@@ -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"
|
||||
|
||||
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.history
|
||||
.vscode
|
||||
*.exe
|
||||
*.pem
|
||||
*.log
|
||||
*.crt
|
||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -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.
|
||||
14
Makefile
Normal file
14
Makefile
Normal file
@@ -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!
|
||||
81
README.md
Normal file
81
README.md
Normal file
@@ -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
|
||||
|
||||
|
||||
37
cache.go
Normal file
37
cache.go
Normal file
@@ -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),
|
||||
}
|
||||
}
|
||||
29
cert.go
Normal file
29
cert.go
Normal file
@@ -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
|
||||
}
|
||||
25
cert_darwin.go
Normal file
25
cert_darwin.go
Normal file
@@ -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
|
||||
}
|
||||
62
cert_linux.go
Normal file
62
cert_linux.go
Normal file
@@ -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")
|
||||
}
|
||||
7
cert_other.go
Normal file
7
cert_other.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func installCA(certPath string) error {
|
||||
return nil
|
||||
}
|
||||
17
cert_windows.go
Normal file
17
cert_windows.go
Normal file
@@ -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
|
||||
}
|
||||
62
config.go
Normal file
62
config.go
Normal file
@@ -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",
|
||||
}
|
||||
16
go.mod
Normal file
16
go.mod
Normal file
@@ -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
|
||||
)
|
||||
32
go.sum
Normal file
32
go.sum
Normal file
@@ -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=
|
||||
17
log.go
Normal file
17
log.go
Normal file
@@ -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()
|
||||
}
|
||||
156
main.go
Normal file
156
main.go
Normal file
@@ -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, "", "")
|
||||
}
|
||||
BIN
rsrc_windows_386.syso
Normal file
BIN
rsrc_windows_386.syso
Normal file
Binary file not shown.
BIN
rsrc_windows_amd64.syso
Normal file
BIN
rsrc_windows_amd64.syso
Normal file
Binary file not shown.
7
run_admin.go
Normal file
7
run_admin.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func runWithAdmin(exePath string, env []string) error {
|
||||
return nil
|
||||
}
|
||||
20
run_admin_darwin.go
Normal file
20
run_admin_darwin.go
Normal file
@@ -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()
|
||||
}
|
||||
15
run_admin_linux.go
Normal file
15
run_admin_linux.go
Normal file
@@ -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()
|
||||
}
|
||||
15
run_admin_windows.go
Normal file
15
run_admin_windows.go
Normal file
@@ -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()
|
||||
}
|
||||
13
script/README_Note.md
Normal file
13
script/README_Note.md
Normal file
@@ -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
|
||||
BIN
script/release-uploader
Normal file
BIN
script/release-uploader
Normal file
Binary file not shown.
5
script/release.json
Normal file
5
script/release.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"tag": "1.0-01",
|
||||
"title": "PreBuild Version 1.0 - 01"
|
||||
}
|
||||
|
||||
7
system_proxy.go
Normal file
7
system_proxy.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func setProxy(enable bool, host string, port string) error {
|
||||
return nil
|
||||
}
|
||||
68
system_proxy_darwin.go
Normal file
68
system_proxy_darwin.go
Normal file
@@ -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
|
||||
}
|
||||
24
system_proxy_linux.go
Normal file
24
system_proxy_linux.go
Normal file
@@ -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
|
||||
}
|
||||
43
system_proxy_windows.go
Normal file
43
system_proxy_windows.go
Normal file
@@ -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
|
||||
}
|
||||
67
utils.go
Normal file
67
utils.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user