12 Commits

Author SHA1 Message Date
Kain344 8264f356a7 feat: update build workflow, enhance README, and modify license for version 1.4-01
Build and Release / release (push) Successful in 1m36s
2026-06-22 11:23:22 +07:00
Kain344 7c3d96f1db fix: correct proxy response status and formatting, and add release configuration file
Build and Release / release (push) Successful in 1m35s
2026-06-12 12:36:43 +07:00
Kain344 2e6f54b7e7 feat: implement EmptyUrls response handling and add release version configuration
Build and Release / release (push) Successful in 1m35s
2026-06-12 12:17:41 +07:00
Kain344 48f801e05b feat: add AlwaysIgnoreUrls for URL filtering; update README and release version to 1.3-01
Build and Release / release (push) Successful in 1m43s
2026-06-12 10:31:40 +07:00
Kain344 5aac0d6297 feat: add automated release publishing script and Gitea workflow integration
Build and Release / release (push) Successful in 1m50s
2026-06-11 20:38:50 +07:00
Kain344 d59288e73f feat: add build and publish automation script for component deployment
Build and Release / release (push) Successful in 1m53s
2026-06-10 12:50:18 +07:00
Kain344 9cdd50f230 feat: add release manifest and enforce certificate file verification in setupCertificate
Build and Release / release (push) Successful in 42s
2026-05-26 18:53:28 +07:00
Kain344 7bcd8b43d9 UPDATE: Add '-no-sys' flag to skip certificate installation and system proxy setup; update README with usage examples; increment release version to 1.2-02
Build and Release / release (push) Successful in 43s
2026-05-26 14:27:36 +07:00
Kain344 77d5a09021 UPDATE: Consolidate release upload steps for Windows and macOS in build workflow
Build and Release / release (push) Successful in 41s
2026-05-24 10:25:05 +07:00
Kain344 321c462f92 fix cicd
Build and Release / release (push) Failing after 41s
2026-05-24 10:22:47 +07:00
Kain344 d3ac27aa5d UPDATE: Enhance macOS support with new build steps, add admin relaunch functionality, and improve proxy management
Build and Release / release (push) Failing after 41s
2026-05-24 10:18:56 +07:00
Kain344 1abf0caee4 UPDATE: Add admin relaunch functionality for macOS and Linux, enhance README with new features and usage examples
Build and Release / release (push) Successful in 18s
2026-05-23 19:53:13 +07:00
31 changed files with 1776 additions and 414 deletions
+24 -8
View File
@@ -17,16 +17,32 @@ jobs:
- 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: Build for Windows x64
run: GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-windows-amd64.exe .
- name: Build for Windows ARM64
run: GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-windows-arm64.exe .
- name: Build for macOS x64
run: GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-macos-amd64 .
- name: Build for macOS ARM64
run: GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-macos-arm64 .
- name: Build for Linux x64
run: GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-linux-amd64 .
- name: Build for Linux ARM64
run: GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o firefly-go-proxy-linux-arm64 .
- name: Grant execute permissions
run: |
chmod +x ./script/release-uploader
chmod +x ./script/publish/publish-script
- name: Upload release
- name: Publish components to API
run: ./script/publish/publish-script
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/FireflyGo_Proxy/releases" -files="firefly-go-proxy.exe"
ENV_ROBOT_TOKEN: ${{ secrets.ENV_ROBOT_TOKEN }}
ENV_GAME_IDS: ${{ secrets.ENV_GAME_IDS }}
ENV_COMPONENT_TYPE: PROXY
ENV_FILES: "firefly-go-proxy-windows-amd64.exe,firefly-go-proxy-windows-arm64.exe,firefly-go-proxy-macos-amd64,firefly-go-proxy-macos-arm64,firefly-go-proxy-linux-amd64,firefly-go-proxy-linux-arm64"
+17
View File
@@ -10,6 +10,7 @@ A lightweight HTTP/HTTPS proxy server with domain redirection and request blocki
- Automatic certificate management
- Cross-platform support (Windows, macOS, Linux)
- System proxy configuration
- Automatic admin prompt on macOS/Linux for certificate/proxy setup
## Installation
@@ -38,7 +39,9 @@ go build
- `-r`: Redirect target host (default: "127.0.0.1:21000")
- `-b`: Comma-separated list of blocked ports
- `-p`: Proxy listen port (default: auto)
- `-e`: Path to an executable to run with admin privileges
- `-no-sys`: Run only the proxy server; skip certificate installation, system proxy setup, and macOS/Linux admin relaunch
### Examples
@@ -66,6 +69,20 @@ go build
./firefly-proxy.exe -e "/path/to/your/executable" //windows
```
5. Start proxy on a specific port:
```bash
./firefly-proxy -p 8888 //linux|macos
./firefly-proxy.exe -p 8888 //windows
```
On macOS/Linux, if the proxy is not already running as root, it relaunches with an administrator prompt. On Linux, logs from the elevated process are written to `/tmp/firefly-go-proxy.log`; on macOS, elevated process output is discarded.
6. Start only the proxy server without changing system settings:
```bash
./firefly-proxy -no-sys -p 8888 //linux|macos
./firefly-proxy.exe -no-sys -p 8888 //windows
```
## How it works
The proxy intercepts HTTP/HTTPS traffic and can:
+77
View File
@@ -0,0 +1,77 @@
//go:build darwin
// +build darwin
package main
import (
"fmt"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
)
func relaunchWithAdminIfNeeded() (bool, error) {
if os.Geteuid() == 0 {
return false, nil
}
exePath, err := os.Executable()
if err != nil {
return false, fmt.Errorf("get executable path: %w", err)
}
workDir, err := os.Getwd()
if err != nil {
return false, fmt.Errorf("get working directory: %w", err)
}
wrapperPID := os.Getpid()
launcherPID := os.Getppid()
args := make([]string, 0, len(os.Args))
args = append(args, shellQuote(exePath))
for _, arg := range os.Args[1:] {
args = append(args, shellQuote(arg))
}
if !hasFlagArg("parent-pid") {
args = append(args, shellQuote("-parent-pid"), shellQuote(strconv.Itoa(wrapperPID)))
}
command := fmt.Sprintf(
"cd %s && %s > /dev/null 2>&1 &",
shellQuote(workDir),
strings.Join(args, " "),
)
script := fmt.Sprintf("do shell script %s with administrator privileges", appleScriptString(command))
if out, err := exec.Command("osascript", "-e", script).CombinedOutput(); err != nil {
return false, formatCommandError("relaunch proxy as admin", err, out)
}
waitForRelaunchedProxyShutdown(launcherPID)
return true, nil
}
func waitForRelaunchedProxyShutdown(launcherPID int) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer signal.Stop(stop)
select {
case <-stop:
case <-parentProcessDone(launcherPID):
}
}
func hasFlagArg(name string) bool {
for _, arg := range os.Args[1:] {
trimmed := strings.TrimLeft(arg, "-")
if trimmed == name || strings.HasPrefix(trimmed, name+"=") {
return true
}
}
return false
}
+73
View File
@@ -0,0 +1,73 @@
//go:build linux
// +build linux
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func relaunchWithAdminIfNeeded() (bool, error) {
if os.Geteuid() == 0 {
return false, nil
}
exePath, err := os.Executable()
if err != nil {
return false, fmt.Errorf("get executable path: %w", err)
}
workDir, err := os.Getwd()
if err != nil {
return false, fmt.Errorf("get working directory: %w", err)
}
args := make([]string, 0, len(os.Args))
args = append(args, shellQuote(exePath))
for _, arg := range os.Args[1:] {
args = append(args, shellQuote(arg))
}
logPath := filepath.Join(os.TempDir(), "firefly-go-proxy.log")
command := fmt.Sprintf(
"cd %s && nohup %s >> %s 2>&1 &",
shellQuote(workDir),
strings.Join(args, " "),
shellQuote(logPath),
)
if pkexecPath, err := exec.LookPath("pkexec"); err == nil {
if out, err := exec.Command(pkexecPath, "sh", "-c", command).CombinedOutput(); err == nil {
return true, nil
} else if _, sudoErr := exec.LookPath("sudo"); sudoErr != nil {
return false, formatCommandError("relaunch proxy as admin with pkexec", err, out)
}
}
sudoPath, err := exec.LookPath("sudo")
if err != nil {
return false, errors.New("pkexec or sudo is required to relaunch with admin privileges")
}
if out, err := exec.Command(sudoPath, "sh", "-c", command).CombinedOutput(); err != nil {
return false, formatCommandError("relaunch proxy as admin with sudo", err, out)
}
return true, nil
}
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}
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)
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin && !linux
// +build !darwin,!linux
package main
func relaunchWithAdminIfNeeded() (bool, error) {
return false, nil
}
+5 -1
View File
@@ -10,13 +10,17 @@ import (
const caCertName = "firefly-go-proxy-ca.crt"
func setupCertificate() (*tls.Certificate, error) {
func setupCertificate(installSystemCA bool) (*tls.Certificate, error) {
if _, err := os.Stat(caCertName); os.IsNotExist(err) {
if err := os.WriteFile(caCertName, goproxy.GoproxyCa.Certificate[0], 0644); err != nil {
return nil, err
}
}
if !installSystemCA {
return &goproxy.GoproxyCa, nil
}
absPath, err := filepath.Abs(caCertName)
if err != nil {
return nil, err
+6
View File
@@ -23,6 +23,12 @@ var AlwaysIgnoreDomains = []string{
"autopatchos.starrails.com",
}
var AlwaysIgnoreUrls = []string{}
var EmptyUrls = []string{
"/query_security_file",
}
var BlockUrls = []string{
"/data_abtest_api/config/experiment/list",
"/common/hkrpg_global/announcement/api/getAlertPic",
+117 -21
View File
@@ -2,11 +2,13 @@ package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
@@ -19,27 +21,75 @@ import (
var ENV_CONFIG = make([]string, 0)
func rawQueryFromRequestURI(requestURI string) string {
queryStart := strings.IndexByte(requestURI, '?')
if queryStart == -1 {
return ""
}
rawQuery := requestURI[queryStart+1:]
if fragmentStart := strings.IndexByte(rawQuery, '#'); fragmentStart != -1 {
rawQuery = rawQuery[:fragmentStart]
}
return rawQuery
}
func parseRedirect(r string) (scheme, host string) {
if strings.Contains(r, "://") {
if u, err := url.Parse(r); err == nil && u.Host != "" {
return u.Scheme, u.Host
}
}
return "http", r
}
func main() {
redirectHost := flag.String("r", "127.0.0.1:21000", "redirect target host")
redirectHost := flag.String("r", "127.0.0.1:21000", "redirect target (host:port or full URL)")
blockedStr := flag.String("b", "", "comma separated list of blocked ports")
proxyPort := flag.Int("p", 0, "proxy listen port (default: auto)")
exePath := flag.String("e", "", "path to the executable")
parentPID := flag.Int("parent-pid", 0, "parent process id to watch")
noSys := flag.Bool("no-sys", false, "skip certificate installation and system proxy setup")
flag.Parse()
redirectScheme, redirectTarget := parseRedirect(*redirectHost)
if !*noSys {
relaunched, err := relaunchWithAdminIfNeeded()
if err != nil {
zlog.Error().Err(err).Msg("Failed to relaunch with admin privileges")
return
}
if relaunched {
zlog.Info().Msg("Relaunched with admin privileges")
return
}
}
blockedPorts := parseBlockedPorts(*blockedStr)
port := findFreePort(blockedPorts)
port := ""
if *proxyPort != 0 {
if *proxyPort < 1 || *proxyPort > 65535 {
zlog.Error().Int("port", *proxyPort).Msg("Invalid proxy port")
return
}
port = fmt.Sprint(*proxyPort)
} else {
port = findFreePort(blockedPorts)
}
if port == "-1" {
zlog.Error().Str("port", port).Msg("No free port available")
return
}
cert, err := setupCertificate()
cert, err := setupCertificate(!*noSys)
if err != nil {
zlog.Error().Err(err).Msg("Failed setup certificate")
return
}
addr := ":" + port
proxyAddr := "127.0.0.1"
proxyEnabled := false
proxyEndpoint := proxyAddr + ":" + port
defer func() {
if r := recover(); r != nil {
@@ -47,18 +97,23 @@ func main() {
Interface("panic", r).
Msg("Unexpected panic")
}
if proxyEnabled {
}()
if !*noSys {
if err := setProxy(true, proxyAddr, port); err != nil {
zlog.Error().Err(err).Msg("Failed to set system proxy")
return
}
stopProxyRefresh := startProxyRefreshLoop(proxyAddr, port)
defer func() {
stopProxyRefresh()
if err := setProxy(false, "", ""); err != nil {
zlog.Error().Err(err).Msg("Failed to reset system proxy")
}
}
}()
if err := setProxy(true, proxyAddr, port); err != nil {
zlog.Error().Err(err).Msg("Failed to set system proxy")
return
}()
} else {
zlog.Info().Msg("System certificate and proxy setup skipped")
}
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) {
@@ -77,6 +132,7 @@ func main() {
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
proxy.CertStore = NewCertStorage()
proxy.OnRequest().HandleConnect(customAlwaysMitm)
@@ -84,11 +140,32 @@ func main() {
proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
host := req.URL.Hostname()
path := req.URL.Path
rawQuery := req.URL.RawQuery
if rawQuery == "" {
rawQuery = rawQueryFromRequestURI(req.RequestURI)
}
if matchDomain(host, AlwaysIgnoreDomains) {
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
return req, nil
}
if matchURL(path, AlwaysIgnoreUrls) {
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
return req, nil
}
if matchURL(path, EmptyUrls) {
full := req.URL.String()
zlog.Warn().Str("url", full).Msg("Empty URL Response")
return req, goproxy.NewResponse(
req,
goproxy.ContentTypeText,
http.StatusNotFound,
"",
)
}
if matchDomain(host, RedirectDomains) {
if matchURL(path, BlockUrls) {
full := req.URL.String()
@@ -97,25 +174,41 @@ func main() {
req,
goproxy.ContentTypeText,
http.StatusNotFound,
`{\n"message": "blocked by proxy",\n,"success": false,\n"retcode": -1\n}`,
`{"message": "blocked by proxy", "success": false, "retcode": -1}`,
)
}
full := req.URL.String()
if matchURL(full, ForceRedirectOnUrlContains) {
if containsURL(full, ForceRedirectOnUrlContains) {
zlog.Info().Str("Url", full).Msg("Force redirect")
zlog.Info().
Str("from_url", full).
Str("raw_query", rawQuery).
Msg("Force redirect")
req.URL.Scheme = "http"
req.URL.Host = *redirectHost
req.URL.Scheme = redirectScheme
req.URL.Host = redirectTarget
req.Host = redirectTarget
req.URL.RawQuery = rawQuery
req.RequestURI = ""
zlog.Info().Str("to_url", req.URL.String()).Msg("Force redirected")
return req, nil
}
zlog.Info().Str("Host", host).Msg("Redirect domain")
req.URL.Scheme = "http"
req.URL.Host = *redirectHost
zlog.Info().
Str("host", host).
Str("from_url", full).
Str("raw_query", rawQuery).
Msg("Redirect domain")
req.URL.Scheme = redirectScheme
req.URL.Host = redirectTarget
req.Host = redirectTarget
req.URL.RawQuery = rawQuery
req.RequestURI = ""
zlog.Info().Str("to_url", req.URL.String()).Msg("Redirected domain")
return req, nil
}
zlog.Warn().Str("url", req.URL.String()).Msg("PASS URL")
return req, nil
})
@@ -131,6 +224,7 @@ func main() {
stop := make(chan os.Signal, 1)
serverErr := make(chan error, 1)
parentDone := parentProcessDone(*parentPID)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
if *exePath != "" && exists(*exePath) {
go func() {
@@ -145,7 +239,7 @@ func main() {
}
go func() {
zlog.Info().
Str("ProxyAddress", proxyAddr).
Str("ProxyAddress", proxyEndpoint).
Str("RedirectTo", *redirectHost).
Str("BlockedPorts", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(blockedPorts)), ","), "[]")).
Str("ExePath", *exePath).
@@ -159,6 +253,8 @@ func main() {
case <-stop:
case err := <-serverErr:
zlog.Error().Err(err).Msg("ListenAndServe failed")
case <-parentDone:
zlog.Info().Int("ParentPID", *parentPID).Msg("Parent process exited")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+36
View File
@@ -0,0 +1,36 @@
//go:build darwin
// +build darwin
package main
import (
"syscall"
"time"
)
func parentProcessDone(pid int) <-chan struct{} {
if pid <= 1 {
return nil
}
done := make(chan struct{})
go func() {
defer close(done)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
if !processExists(pid) {
return
}
}
}()
return done
}
func processExists(pid int) bool {
err := syscall.Kill(pid, 0)
return err == nil || err == syscall.EPERM
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin
// +build !darwin
package main
func parentProcessDone(pid int) <-chan struct{} {
return nil
}
+42
View File
@@ -0,0 +1,42 @@
//go:build darwin
// +build darwin
package main
import (
"sync"
"time"
zlog "github.com/rs/zerolog/log"
)
func startProxyRefreshLoop(host string, port string) func() {
done := make(chan struct{})
stopped := make(chan struct{})
var once sync.Once
go func() {
defer close(stopped)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
if err := setProxy(true, host, port); err != nil {
zlog.Error().Err(err).Msg("Failed to refresh macOS proxy services")
}
}
}
}()
return func() {
once.Do(func() {
close(done)
<-stopped
})
}
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !darwin
// +build !darwin
package main
func startProxyRefreshLoop(host string, port string) func() {
return func() {}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Changelog
### UPDATE
- Go 1.26.1, latest lib
- Support linux, macos, window
Binary file not shown.
+705
View File
@@ -0,0 +1,705 @@
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
const (
apiRequestTimeout = 45 * time.Second
maxHTTPAttempts = 5
)
// API responses structures
type CommonResponse struct {
Status bool `json:"status"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
Errors any `json:"errors"`
}
type AuthResponse struct {
AccessToken string `json:"access_token"`
}
type PreSignedResponse struct {
TokenID string `json:"token_id"`
UploadUrl string `json:"upload_url"`
StorageKey string `json:"storage_key"`
SignedHeaders map[string]string `json:"signed_headers"`
}
type MediaResponse struct {
ID string `json:"id"`
StorageKey string `json:"storage_key"`
OriginalName string `json:"original_name"`
MimeType string `json:"mime_type"`
Size int64 `json:"size"`
}
type PreSignedCompleteDto struct {
TokenID string `json:"token_id"`
FileMetadata json.RawMessage `json:"file_metadata,omitempty"`
}
type CreateComponentRequest struct {
Type string `json:"type"`
Platform string `json:"platform"`
Status string `json:"status"`
Version string `json:"version"`
Description *string `json:"description,omitempty"`
Hash *string `json:"hash,omitempty"`
MediaIDs []string `json:"media_ids"`
GameIDs []string `json:"game_ids"`
}
type UpdateComponentRequest struct {
Type *string `json:"type,omitempty"`
Platform *string `json:"platform,omitempty"`
Status *string `json:"status,omitempty"`
Version *string `json:"version,omitempty"`
Description *string `json:"description,omitempty"`
Hash *string `json:"hash,omitempty"`
MediaIDs []string `json:"media_ids,omitempty"`
GameIDs []string `json:"game_ids,omitempty"`
}
func readFile(path string) string {
data, err := os.ReadFile(path)
if err != nil {
panic(fmt.Sprintf("Failed to read %s: %v", path, err))
}
return string(data)
}
func main() {
// Flag definitions (useful for manual testing, but optional in CI/CD)
apiURLFlag := flag.String("api-url", "https://api.punklorde.org", "Base URL of the management API")
gameIdsFlag := flag.String("game-ids", "", "Comma-separated Game IDs (defaults to ENV_GAME_IDS)")
filesFlag := flag.String("files", "", "Comma-separated list of files to upload (defaults to scanning prebuild/)")
cTypeFlag := flag.String("type", "", "Component type: LAUNCHER, PROXY, SERVER (defaults to ENV_COMPONENT_TYPE or PROXY)")
flag.Parse()
// 1. Resolve settings from Env or Flags
apiURL := *apiURLFlag
if envAPI := os.Getenv("ENV_API_URL"); envAPI != "" {
apiURL = envAPI
}
gameIdsStr := *gameIdsFlag
if gameIdsStr == "" {
gameIdsStr = os.Getenv("ENV_GAME_IDS")
}
if gameIdsStr == "" {
fmt.Fprintln(os.Stderr, "Error: game IDs must be specified via -game-ids flag or ENV_GAME_IDS environment variable")
os.Exit(1)
}
gameIDs := splitCommaSeparated(gameIdsStr)
cType := *cTypeFlag
if cType == "" {
cType = os.Getenv("ENV_COMPONENT_TYPE")
}
if cType == "" {
cType = "PROXY" // Default component type
}
// 2. Read release metadata from files
releaseJSON := readFile("script/release.json")
var meta map[string]string
if err := json.Unmarshal([]byte(releaseJSON), &meta); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse release.json: %v\n", err)
os.Exit(1)
}
version := meta["tag"]
if version == "" {
fmt.Fprintln(os.Stderr, "Error: 'tag' is missing in release.json")
os.Exit(1)
}
var description *string
if bodyBytes, err := os.ReadFile("script/README_Note.md"); err == nil && len(bodyBytes) > 0 {
descStr := string(bodyBytes)
description = &descStr
}
// 3. Read robot token from environment
robotToken := os.Getenv("ENV_ROBOT_TOKEN")
if robotToken == "" {
fmt.Fprintln(os.Stderr, "Error: ENV_ROBOT_TOKEN environment variable is missing")
os.Exit(1)
}
fmt.Println("Refreshing access token using robot token...")
accessToken, err := refreshRobotToken(apiURL, robotToken)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to refresh token: %v\n", err)
os.Exit(1)
}
fmt.Println("Access token successfully obtained.")
// 4. Resolve files to publish
var filePaths []string
filesStr := *filesFlag
if filesStr == "" {
filesStr = os.Getenv("ENV_FILES")
}
if filesStr != "" {
filePaths = splitCommaSeparated(filesStr)
} else {
// Fallback to scanning prebuild/
prebuildDir := "prebuild"
files, err := os.ReadDir(prebuildDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot read prebuild folder: %v\n", err)
os.Exit(1)
}
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".zip" {
filePaths = append(filePaths, filepath.Join(prebuildDir, file.Name()))
}
}
}
var processedCount int
for _, filePath := range filePaths {
fileName := filepath.Base(filePath)
fmt.Printf("\n--- Processing asset: %s ---\n", fileName)
// Map filename to platform
platform := detectPlatformFromFilename(fileName)
fmt.Printf("Mapped Platform: %s\n", platform)
// Calculate size and SHA256
size, hash, err := getFileInfoAndHash(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileName, err)
os.Exit(1)
}
fmt.Printf("Size: %d bytes, SHA256: %s\n", size, hash)
// Request presigned URL
contentType := "application/octet-stream"
if filepath.Ext(fileName) == ".zip" {
contentType = "application/zip"
}
fmt.Println("Requesting presigned URL...")
presigned, err := getPresignedURL(apiURL, accessToken, fileName, contentType, size)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get presigned URL: %v\n", err)
os.Exit(1)
}
// Upload file to S3
fmt.Println("Uploading file to storage...")
err = uploadFileToS3(filePath, presigned)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to upload file to storage: %v\n", err)
os.Exit(1)
}
// Confirm upload completion
fmt.Println("Confirming upload completion...")
mediaID, err := completePreSignedUpload(apiURL, accessToken, presigned.TokenID, hash)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to complete upload: %v\n", err)
os.Exit(1)
}
fmt.Printf("Media ID generated: %s\n", mediaID)
fmt.Println("Checking if component already exists...")
existingID, err := findExistingComponent(apiURL, accessToken, cType, platform, version)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to check existing component: %v\n", err)
}
var compID string
if existingID != "" {
fmt.Printf("Component already exists with ID: %s. Updating it...\n", existingID)
updateReq := UpdateComponentRequest{
Type: &cType,
Platform: &platform,
Description: description,
Hash: &hash,
MediaIDs: []string{mediaID},
GameIDs: gameIDs,
}
compID, err = updateComponent(apiURL, accessToken, existingID, updateReq)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to update component: %v\n", err)
os.Exit(1)
}
fmt.Printf("SUCCESS! Component updated with ID: %s for Platform: %s\n", compID, platform)
} else {
fmt.Println("Component does not exist. Registering on the API...")
reqBody := CreateComponentRequest{
Type: cType,
Platform: platform,
Status: "ACTIVE",
Version: version,
Description: description,
Hash: &hash,
MediaIDs: []string{mediaID},
GameIDs: gameIDs,
}
compID, err = createComponent(apiURL, accessToken, reqBody)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to register component: %v\n", err)
os.Exit(1)
}
fmt.Printf("SUCCESS! Component created with ID: %s for Platform: %s\n", compID, platform)
}
processedCount++
}
if processedCount == 0 {
fmt.Println("No component files found to publish.")
} else {
fmt.Printf("\nAll %d components successfully published to API.\n", processedCount)
}
}
func detectPlatformFromFilename(name string) string {
nameLower := strings.ToLower(name)
if strings.Contains(nameLower, "mac_arm") || strings.Contains(nameLower, "macos-arm64") {
return "MACOS_ARM64"
}
if strings.Contains(nameLower, "mac_x86") || strings.Contains(nameLower, "macos-amd64") {
return "MACOS_X64"
}
if strings.Contains(nameLower, "win_arm") || strings.Contains(nameLower, "win-arm") || strings.Contains(nameLower, "windows-arm") {
return "WINDOWS_ARM64"
}
if strings.Contains(nameLower, "win_x86") || strings.Contains(nameLower, "win_x64") || strings.Contains(nameLower, "win") {
return "WINDOWS_X64"
}
if strings.Contains(nameLower, "android_arm64") {
return "ANDROID_ARM64"
}
if strings.Contains(nameLower, "linux_x64") || strings.Contains(nameLower, "linux-amd64") || strings.Contains(nameLower, "linux-x64") {
return "LINUX_X64"
}
if strings.Contains(nameLower, "linux_arm64") || strings.Contains(nameLower, "linux-arm64") {
return "LINUX_ARM64"
}
return "WINDOWS_X64" // Fallback default
}
func splitCommaSeparated(s string) []string {
parts := strings.Split(s, ",")
var result []string
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}
func getFileInfoAndHash(path string) (int64, string, error) {
file, err := os.Open(path)
if err != nil {
return 0, "", err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return 0, "", err
}
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return 0, "", err
}
return stat.Size(), hex.EncodeToString(hasher.Sum(nil)), nil
}
func apiEndpoint(apiURL, path string) string {
return strings.TrimRight(apiURL, "/") + path
}
func retryDelay(attempt int) time.Duration {
delay := time.Duration(1<<uint(attempt-1)) * time.Second
if delay > 20*time.Second {
return 20 * time.Second
}
return delay
}
func isRetryableStatus(statusCode int) bool {
return statusCode == http.StatusRequestTimeout ||
statusCode == http.StatusTooEarly ||
statusCode == http.StatusTooManyRequests ||
statusCode >= http.StatusInternalServerError
}
func isAccepted(statusCode int, accepted ...int) bool {
for _, code := range accepted {
if statusCode == code {
return true
}
}
return false
}
func doHTTPWithRetry(label string, client *http.Client, buildRequest func() (*http.Request, error), accepted ...int) ([]byte, int, error) {
var lastErr error
for attempt := 1; attempt <= maxHTTPAttempts; attempt++ {
req, err := buildRequest()
if err != nil {
return nil, 0, err
}
resp, err := client.Do(req)
if err != nil {
lastErr = err
if attempt < maxHTTPAttempts {
delay := retryDelay(attempt)
fmt.Fprintf(os.Stderr, "%s attempt %d/%d failed: %v. Retrying in %s...\n", label, attempt, maxHTTPAttempts, err, delay)
time.Sleep(delay)
continue
}
return nil, 0, err
}
bodyBytes, readErr := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if readErr != nil {
return nil, resp.StatusCode, readErr
}
if isAccepted(resp.StatusCode, accepted...) {
return bodyBytes, resp.StatusCode, nil
}
if !isRetryableStatus(resp.StatusCode) || attempt == maxHTTPAttempts {
return bodyBytes, resp.StatusCode, nil
}
lastErr = fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(bodyBytes)))
delay := retryDelay(attempt)
fmt.Fprintf(os.Stderr, "%s attempt %d/%d returned %v. Retrying in %s...\n", label, attempt, maxHTTPAttempts, lastErr, delay)
time.Sleep(delay)
}
return nil, 0, lastErr
}
func refreshRobotToken(apiURL, robotToken string) (string, error) {
endpoint := apiEndpoint(apiURL, "/robot-tokens/refresh")
client := &http.Client{Timeout: apiRequestTimeout}
bodyBytes, statusCode, err := doHTTPWithRetry("refresh robot token", client, func() (*http.Request, error) {
req, err := http.NewRequest("POST", endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+robotToken)
return req, nil
}, http.StatusOK)
if err != nil {
return "", err
}
if statusCode != http.StatusOK {
return "", fmt.Errorf("refresh token failed with status %d: %s", statusCode, string(bodyBytes))
}
var cr CommonResponse
if err := json.Unmarshal(bodyBytes, &cr); err != nil {
return "", fmt.Errorf("failed to parse common response: %w", err)
}
if !cr.Status {
return "", fmt.Errorf("API error: %s", cr.Message)
}
var auth AuthResponse
if err := json.Unmarshal(cr.Data, &auth); err != nil {
return "", fmt.Errorf("failed to parse auth response data: %w", err)
}
return auth.AccessToken, nil
}
func getPresignedURL(apiURL, accessToken, fileName, contentType string, size int64) (*PreSignedResponse, error) {
endpoint, err := url.Parse(apiEndpoint(apiURL, "/media/presigned"))
if err != nil {
return nil, err
}
query := endpoint.Query()
query.Set("fileName", fileName)
query.Set("content_type", contentType)
query.Set("size", strconv.FormatInt(size, 10))
endpoint.RawQuery = query.Encode()
client := &http.Client{Timeout: apiRequestTimeout}
bodyBytes, statusCode, err := doHTTPWithRetry("get presigned URL", client, func() (*http.Request, error) {
req, err := http.NewRequest("GET", endpoint.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
return req, nil
}, http.StatusOK)
if err != nil {
return nil, err
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get presigned URL (status %d): %s", statusCode, string(bodyBytes))
}
var cr CommonResponse
if err := json.Unmarshal(bodyBytes, &cr); err != nil {
return nil, fmt.Errorf("failed to parse common response: %w", err)
}
if !cr.Status {
return nil, fmt.Errorf("API error: %s", cr.Message)
}
var presigned PreSignedResponse
if err := json.Unmarshal(cr.Data, &presigned); err != nil {
return nil, fmt.Errorf("failed to parse presigned data: %w", err)
}
return &presigned, nil
}
func uploadFileToS3(path string, presigned *PreSignedResponse) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return err
}
req, err := http.NewRequest("PUT", presigned.UploadUrl, file)
if err != nil {
return err
}
// Set S3 signature headers
for k, v := range presigned.SignedHeaders {
req.Header.Set(k, v)
}
req.ContentLength = stat.Size()
if req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/zip")
}
client := &http.Client{Timeout: 10 * time.Minute}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("S3 upload failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
return nil
}
func completePreSignedUpload(apiURL, accessToken, tokenID, hash string) (string, error) {
endpoint := apiEndpoint(apiURL, "/media/presigned/complete")
metaJSON, _ := json.Marshal(map[string]string{"sha256": hash})
dto := PreSignedCompleteDto{
TokenID: tokenID,
FileMetadata: json.RawMessage(metaJSON),
}
payload, _ := json.Marshal(dto)
client := &http.Client{Timeout: apiRequestTimeout}
bodyBytes, statusCode, err := doHTTPWithRetry("complete presigned upload", client, func() (*http.Request, error) {
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
return req, nil
}, http.StatusOK)
if err != nil {
return "", err
}
if statusCode != http.StatusOK {
return "", fmt.Errorf("complete upload failed (status %d): %s", statusCode, string(bodyBytes))
}
var cr CommonResponse
if err := json.Unmarshal(bodyBytes, &cr); err != nil {
return "", fmt.Errorf("failed to parse common response: %w", err)
}
if !cr.Status {
return "", fmt.Errorf("API error: %s", cr.Message)
}
var media MediaResponse
if err := json.Unmarshal(cr.Data, &media); err != nil {
return "", fmt.Errorf("failed to parse media data: %w", err)
}
return media.ID, nil
}
func createComponent(apiURL, accessToken string, dto CreateComponentRequest) (string, error) {
endpoint := apiEndpoint(apiURL, "/components")
payload, _ := json.Marshal(dto)
client := &http.Client{Timeout: apiRequestTimeout}
bodyBytes, statusCode, err := doHTTPWithRetry("create component", client, func() (*http.Request, error) {
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
return req, nil
}, http.StatusCreated, http.StatusOK)
if err != nil {
return "", err
}
if statusCode != http.StatusCreated && statusCode != http.StatusOK {
return "", fmt.Errorf("create component failed (status %d): %s", statusCode, string(bodyBytes))
}
var cr CommonResponse
if err := json.Unmarshal(bodyBytes, &cr); err != nil {
return "", fmt.Errorf("failed to parse common response: %w", err)
}
if !cr.Status {
return "", fmt.Errorf("API error: %s", cr.Message)
}
var component struct {
ID string `json:"id"`
}
if err := json.Unmarshal(cr.Data, &component); err != nil {
return "", fmt.Errorf("failed to parse component data: %w", err)
}
return component.ID, nil
}
func findExistingComponent(apiURL, accessToken, cType, platform, version string) (string, error) {
endpoint, err := url.Parse(apiEndpoint(apiURL, "/components"))
if err != nil {
return "", err
}
query := endpoint.Query()
query.Set("type", cType)
query.Set("platform", platform)
query.Set("search", version)
endpoint.RawQuery = query.Encode()
client := &http.Client{Timeout: apiRequestTimeout}
bodyBytes, statusCode, err := doHTTPWithRetry("search existing component", client, func() (*http.Request, error) {
req, err := http.NewRequest("GET", endpoint.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
return req, nil
}, http.StatusOK)
if err != nil {
return "", err
}
if statusCode != http.StatusOK {
return "", fmt.Errorf("failed to search components (status %d): %s", statusCode, string(bodyBytes))
}
var pr struct {
Status bool `json:"status"`
Data []struct {
ID string `json:"id"`
ComponentType string `json:"component_type"`
Platform string `json:"platform"`
Version string `json:"version"`
} `json:"data"`
}
if err := json.Unmarshal(bodyBytes, &pr); err != nil {
return "", fmt.Errorf("failed to parse components search response: %w", err)
}
if !pr.Status {
return "", fmt.Errorf("search components returned unsuccessful status")
}
for _, item := range pr.Data {
if item.ComponentType == cType && item.Platform == platform && item.Version == version {
return item.ID, nil
}
}
return "", nil
}
func updateComponent(apiURL, accessToken, id string, dto UpdateComponentRequest) (string, error) {
endpoint := apiEndpoint(apiURL, "/components/"+url.PathEscape(id))
payload, _ := json.Marshal(dto)
client := &http.Client{Timeout: apiRequestTimeout}
bodyBytes, statusCode, err := doHTTPWithRetry("update component", client, func() (*http.Request, error) {
req, err := http.NewRequest("PUT", endpoint, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")
return req, nil
}, http.StatusOK)
if err != nil {
return "", err
}
if statusCode != http.StatusOK {
return "", fmt.Errorf("update component failed (status %d): %s", statusCode, string(bodyBytes))
}
var cr CommonResponse
if err := json.Unmarshal(bodyBytes, &cr); err != nil {
return "", fmt.Errorf("failed to parse common response: %w", err)
}
if !cr.Status {
return "", fmt.Errorf("API error: %s", cr.Message)
}
var component struct {
ID string `json:"id"`
}
if err := json.Unmarshal(cr.Data, &component); err != nil {
return "", fmt.Errorf("failed to parse component data: %w", err)
}
return component.ID, nil
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"tag": "1.1-02",
"title": "PreBuild Version 1.1 - 02"
"tag": "1.4-01",
"title": "PreBuild Version 1.4 - 01"
}
+271 -14
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"os/exec"
"strconv"
"strings"
"sync"
)
@@ -26,6 +27,7 @@ var darwinProxyState = struct {
sync.Mutex
captured bool
previous map[string]darwinProxySettings
applied map[string]struct{}
}{}
func enabledNetworkServices() ([]string, error) {
@@ -50,6 +52,171 @@ func enabledNetworkServices() ([]string, error) {
return services, nil
}
func proxyNetworkServices() ([]string, error) {
service, defaultErr := defaultNetworkService()
if defaultErr == nil && service != "" {
return []string{service}, nil
}
services, activeErr := activeNetworkServices()
if activeErr == nil && len(services) > 0 {
return services, nil
}
if defaultErr != nil || activeErr != nil {
return nil, errors.Join(defaultErr, activeErr)
}
return nil, fmt.Errorf("no active network services found")
}
func defaultNetworkService() (string, error) {
device, err := defaultNetworkDevice()
if err != nil {
return "", err
}
servicesByDevice, err := networkServicesByDevice()
if err != nil {
return "", err
}
service := servicesByDevice[device]
if service == "" {
return "", fmt.Errorf("network service for default device %s not found", device)
}
return service, nil
}
func defaultNetworkDevice() (string, error) {
out, err := exec.Command("route", "-n", "get", "default").CombinedOutput()
if err != nil {
return "", formatCommandError("get default network route", err, out)
}
for _, line := range strings.Split(string(out), "\n") {
key, value, ok := strings.Cut(line, ":")
if ok && strings.TrimSpace(key) == "interface" {
device := strings.TrimSpace(value)
if device != "" {
return device, nil
}
}
}
return "", fmt.Errorf("default network interface not found")
}
func networkServicesByDevice() (map[string]string, error) {
out, err := exec.Command("networksetup", "-listnetworkserviceorder").CombinedOutput()
if err != nil {
return nil, formatCommandError("list network service order", err, out)
}
services := make(map[string]string)
currentService := ""
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if service, ok := parseNetworkServiceOrderName(line); ok {
currentService = service
continue
}
if currentService == "" {
continue
}
if device, ok := parseNetworkServiceOrderDevice(line); ok {
services[device] = currentService
currentService = ""
}
}
if len(services) == 0 {
return nil, fmt.Errorf("no network service devices found")
}
return services, nil
}
func parseNetworkServiceOrderName(line string) (string, bool) {
if !strings.HasPrefix(line, "(") {
return "", false
}
end := strings.Index(line, ")")
if end <= 1 {
return "", false
}
if _, err := strconv.Atoi(line[1:end]); err != nil {
return "", false
}
service := strings.TrimSpace(line[end+1:])
return service, service != ""
}
func parseNetworkServiceOrderDevice(line string) (string, bool) {
_, value, ok := strings.Cut(line, "Device:")
if !ok {
return "", false
}
value = strings.TrimSpace(value)
if idx := strings.IndexAny(value, ",)"); idx != -1 {
value = value[:idx]
}
device := strings.TrimSpace(value)
return device, device != ""
}
func activeNetworkServices() ([]string, error) {
services, err := enabledNetworkServices()
if err != nil {
return nil, err
}
active := make([]string, 0, len(services))
for _, service := range services {
ok, err := isNetworkServiceActive(service)
if err != nil {
continue
}
if ok {
active = append(active, service)
}
}
if len(active) == 0 {
return nil, fmt.Errorf("no active network services found")
}
return active, nil
}
func isNetworkServiceActive(service string) (bool, error) {
out, err := exec.Command("networksetup", "-getinfo", service).CombinedOutput()
if err != nil {
return false, formatCommandError("get network service info "+service, err, out)
}
for _, line := range strings.Split(string(out), "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
key = strings.TrimSpace(key)
if key != "IP address" && key != "IPv6 IP address" {
continue
}
value = strings.TrimSpace(value)
if value != "" && !strings.EqualFold(value, "none") {
return true, nil
}
}
return false, nil
}
func getProxySettings(service string) (darwinProxySettings, error) {
web, webErr := getProxyEndpoint("-getwebproxy", service)
secure, secureErr := getProxyEndpoint("-getsecurewebproxy", service)
@@ -166,12 +333,86 @@ func captureProxySettings(services []string) (map[string]darwinProxySettings, er
return settings, nil
}
func setProxy(enable bool, host string, port string) error {
services, err := enabledNetworkServices()
func captureMissingProxySettings(services []string) error {
missing := make([]string, 0, len(services))
for _, service := range services {
if _, ok := darwinProxyState.previous[service]; !ok {
missing = append(missing, service)
}
}
if len(missing) == 0 {
return nil
}
settings, err := captureProxySettings(missing)
if err != nil {
return err
}
for service, setting := range settings {
darwinProxyState.previous[service] = setting
}
return nil
}
func serviceSet(services []string) map[string]struct{} {
set := make(map[string]struct{}, len(services))
for _, service := range services {
set[service] = struct{}{}
}
return set
}
func appliedServicesNotIn(selected map[string]struct{}) []string {
services := make([]string, 0, len(darwinProxyState.applied))
for service := range darwinProxyState.applied {
if _, ok := selected[service]; !ok {
services = append(services, service)
}
}
return services
}
func appliedServices() []string {
services := make([]string, 0, len(darwinProxyState.applied))
for service := range darwinProxyState.applied {
services = append(services, service)
}
return services
}
func proxySettingsForServices(settings map[string]darwinProxySettings, services []string) map[string]darwinProxySettings {
filtered := make(map[string]darwinProxySettings, len(services))
for _, service := range services {
if setting, ok := settings[service]; ok {
filtered[service] = setting
}
}
return filtered
}
func resetDarwinProxyState() {
darwinProxyState.previous = nil
darwinProxyState.applied = nil
darwinProxyState.captured = false
}
func restoreAppliedProxySettings() error {
if len(darwinProxyState.applied) == 0 {
return nil
}
err := restoreProxySettings(proxySettingsForServices(
darwinProxyState.previous,
appliedServices(),
))
if err == nil {
darwinProxyState.applied = nil
}
return err
}
func setProxy(enable bool, host string, port string) error {
darwinProxyState.Lock()
defer darwinProxyState.Unlock()
@@ -180,32 +421,48 @@ func setProxy(enable bool, host string, port string) error {
return fmt.Errorf("host and port are required to enable proxy")
}
if !darwinProxyState.captured {
settings, err := captureProxySettings(services)
if err != nil {
return err
}
darwinProxyState.previous = settings
darwinProxyState.captured = true
services, err := proxyNetworkServices()
if err != nil {
return errors.Join(err, restoreAppliedProxySettings())
}
if darwinProxyState.previous == nil {
darwinProxyState.previous = make(map[string]darwinProxySettings)
}
if err := captureMissingProxySettings(services); err != nil {
return err
}
darwinProxyState.captured = true
if err := setProxyForServices(services, host, port); err != nil {
restoreErr := restoreProxySettings(darwinProxyState.previous)
darwinProxyState.previous = nil
darwinProxyState.captured = false
resetDarwinProxyState()
return errors.Join(err, restoreErr)
}
return nil
selected := serviceSet(services)
removed := appliedServicesNotIn(selected)
restoreErr := restoreProxySettings(proxySettingsForServices(darwinProxyState.previous, removed))
if restoreErr != nil {
for _, service := range removed {
selected[service] = struct{}{}
}
}
darwinProxyState.applied = selected
return restoreErr
}
if darwinProxyState.captured {
err := restoreProxySettings(darwinProxyState.previous)
if err == nil {
darwinProxyState.previous = nil
darwinProxyState.captured = false
resetDarwinProxyState()
}
return err
}
services, err := enabledNetworkServices()
if err != nil {
return err
}
return disableProxyForServices(services)
}
+9
View File
@@ -26,6 +26,15 @@ func matchURL(url string, list []string) bool {
return false
}
func containsURL(url string, list []string) bool {
for _, u := range list {
if strings.Contains(url, u) {
return true
}
}
return false
}
func cleanHost(h string) string {
if idx := strings.LastIndex(h, ":"); idx != -1 {
return h[:idx]