Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8264f356a7 | |||
| 7c3d96f1db | |||
| 2e6f54b7e7 | |||
| 48f801e05b | |||
| 5aac0d6297 | |||
| d59288e73f | |||
| 9cdd50f230 |
+48
-46
@@ -1,46 +1,48 @@
|
||||
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: Build for macOS Intel
|
||||
run: |
|
||||
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
|
||||
go build -trimpath -ldflags="-s -w" \
|
||||
-o firefly-go-proxy-macos-amd64 .
|
||||
|
||||
- name: Build for macOS Apple Silicon
|
||||
run: |
|
||||
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 \
|
||||
go build -trimpath -ldflags="-s -w" \
|
||||
-o firefly-go-proxy-macos-arm64 .
|
||||
|
||||
- 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/FireflyGo_Proxy/releases" \
|
||||
-files="firefly-go-proxy.exe,firefly-go-proxy-macos-amd64,firefly-go-proxy-macos-arm64"
|
||||
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 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/publish/publish-script
|
||||
|
||||
- name: Publish components to API
|
||||
run: ./script/publish/publish-script
|
||||
env:
|
||||
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"
|
||||
|
||||
@@ -1,18 +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.
|
||||
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.
|
||||
|
||||
@@ -25,9 +25,9 @@ build_mac_arm64:
|
||||
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!
|
||||
|
||||
set_logo:
|
||||
@echo Embedding application icon...
|
||||
go-winres simply --icon ./logo.ico
|
||||
@echo Done!
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
# 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)
|
||||
# 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
|
||||
- Automatic admin prompt on macOS/Linux for certificate/proxy setup
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
### 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
|
||||
- `-p`: Proxy listen port (default: auto)
|
||||
@@ -44,25 +44,25 @@ go build
|
||||
- `-no-sys`: Run only the proxy server; skip certificate installation, system proxy setup, and macOS/Linux admin relaunch
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
|
||||
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
|
||||
@@ -82,17 +82,17 @@ go build
|
||||
./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:
|
||||
- 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
|
||||
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
|
||||
@@ -1,37 +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),
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,16 @@ import (
|
||||
const caCertName = "firefly-go-proxy-ca.crt"
|
||||
|
||||
func setupCertificate(installSystemCA bool) (*tls.Certificate, error) {
|
||||
if !installSystemCA {
|
||||
return &goproxy.GoproxyCa, nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+62
-62
@@ -1,62 +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")
|
||||
}
|
||||
//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
-7
@@ -1,7 +1,7 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func installCA(certPath string) error {
|
||||
return nil
|
||||
}
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func installCA(certPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+17
-17
@@ -1,17 +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
|
||||
}
|
||||
//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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,17 +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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
@@ -32,8 +34,17 @@ func rawQueryFromRequestURI(requestURI string) string {
|
||||
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")
|
||||
@@ -41,6 +52,8 @@ func main() {
|
||||
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 {
|
||||
@@ -119,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)
|
||||
@@ -136,6 +150,22 @@ func main() {
|
||||
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()
|
||||
@@ -144,7 +174,7 @@ 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()
|
||||
@@ -155,8 +185,9 @@ func main() {
|
||||
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")
|
||||
@@ -168,8 +199,9 @@ func main() {
|
||||
Str("from_url", full).
|
||||
Str("raw_query", rawQuery).
|
||||
Msg("Redirect domain")
|
||||
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("Redirected domain")
|
||||
@@ -230,4 +262,4 @@ func main() {
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
zlog.Error().Err(err).Msg("Server shutdown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func runWithAdmin(exePath string, env []string) error {
|
||||
return nil
|
||||
}
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func runWithAdmin(exePath string, env []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+15
-15
@@ -1,15 +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()
|
||||
}
|
||||
//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
-15
@@ -1,15 +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()
|
||||
}
|
||||
//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()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Changelog
|
||||
|
||||
### UPDATE
|
||||
- Fix bug in macos
|
||||
# Changelog
|
||||
|
||||
### UPDATE
|
||||
- Support linux, macos, window
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"tag": "1.2-02",
|
||||
"title": "PreBuild Version 1.2 - 02"
|
||||
}
|
||||
{
|
||||
"tag": "1.4-01",
|
||||
"title": "PreBuild Version 1.4 - 01"
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func setProxy(enable bool, host string, port string) error {
|
||||
return nil
|
||||
//go:build !windows && !darwin && !linux
|
||||
|
||||
package main
|
||||
|
||||
func setProxy(enable bool, host string, port string) error {
|
||||
return nil
|
||||
}
|
||||
+24
-24
@@ -1,24 +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
|
||||
}
|
||||
//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
-43
@@ -1,43 +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
|
||||
}
|
||||
//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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user