Fix the abnormal path retrieval issue in some systems.

This commit is contained in:
2026-04-09 01:00:31 +08:00
parent 76f0d815aa
commit 792f547dc3
7 changed files with 453 additions and 336 deletions

View File

@@ -2,6 +2,7 @@ package cmd
import ( import (
"NetworkAuth/config" "NetworkAuth/config"
"NetworkAuth/utils"
"NetworkAuth/utils/logger" "NetworkAuth/utils/logger"
"io" "io"
"os" "os"
@@ -67,7 +68,7 @@ func setupLogrusForNonHTTP() {
if cfgFile != "" { if cfgFile != "" {
config.Init(cfgFile) config.Init(cfgFile)
} else { } else {
config.Init("./config.json") config.Init("config.json")
} }
// 根据配置文件进一步配置logrus // 根据配置文件进一步配置logrus
@@ -105,7 +106,11 @@ func setupLogrusFromConfig() {
logFile := viper.GetString("log.file") logFile := viper.GetString("log.file")
if logFile != "" { if logFile != "" {
// 确保日志目录存在 // 确保日志目录存在
logDir := filepath.Dir(logFile) path := logFile
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
}
logDir := filepath.Dir(path)
if err := os.MkdirAll(logDir, 0755); err != nil { if err := os.MkdirAll(logDir, 0755); err != nil {
logrus.WithError(err).Error("创建日志目录失败") logrus.WithError(err).Error("创建日志目录失败")
return return
@@ -113,7 +118,7 @@ func setupLogrusFromConfig() {
// 配置lumberjack日志轮转 // 配置lumberjack日志轮转
lumberjackLogger := &lumberjack.Logger{ lumberjackLogger := &lumberjack.Logger{
Filename: logFile, Filename: path,
MaxSize: viper.GetInt("log.max_size"), // MB MaxSize: viper.GetInt("log.max_size"), // MB
MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量 MaxBackups: viper.GetInt("log.max_backups"), // 保留的旧日志文件数量
MaxAge: viper.GetInt("log.max_age"), // 天数 MaxAge: viper.GetInt("log.max_age"), // 天数

View File

@@ -6,6 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"NetworkAuth/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -107,7 +109,7 @@ func GetDefaultAppConfig() *AppConfig {
MaxOpenConns: 100, MaxOpenConns: 100,
}, },
SQLite: SQLiteConfig{ SQLite: SQLiteConfig{
Path: "./database.db", Path: "database.db",
}, },
}, },
Redis: RedisConfig{ Redis: RedisConfig{
@@ -118,7 +120,7 @@ func GetDefaultAppConfig() *AppConfig {
}, },
Log: LogConfig{ Log: LogConfig{
Level: "info", Level: "info",
File: "./logs/app.log", File: "logs/app.log",
MaxSize: 100, MaxSize: 100,
MaxBackups: 5, MaxBackups: 5,
MaxAge: 30, MaxAge: 30,
@@ -128,6 +130,9 @@ func GetDefaultAppConfig() *AppConfig {
// Init 初始化配置文件 // Init 初始化配置文件
func Init(cfgFilePath string) { func Init(cfgFilePath string) {
if !filepath.IsAbs(cfgFilePath) {
cfgFilePath = filepath.Join(utils.GetRootDir(), cfgFilePath)
}
currentConfigFilePath = cfgFilePath currentConfigFilePath = cfgFilePath
viper.SetConfigFile(cfgFilePath) viper.SetConfigFile(cfgFilePath)
viper.SetConfigType("json") viper.SetConfigType("json")
@@ -204,7 +209,10 @@ func SaveConfig(appConfig *AppConfig) error {
return err return err
} }
if currentConfigFilePath == "" { if currentConfigFilePath == "" {
currentConfigFilePath = "./config.json" currentConfigFilePath = "config.json"
}
if !filepath.IsAbs(currentConfigFilePath) {
currentConfigFilePath = filepath.Join(utils.GetRootDir(), currentConfigFilePath)
} }
if err := os.MkdirAll(filepath.Dir(currentConfigFilePath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(currentConfigFilePath), 0755); err != nil {
return err return err

View File

@@ -9,6 +9,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"NetworkAuth/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -125,8 +127,13 @@ func validateSQLiteConfig(config *SQLiteConfig) error {
return errors.New("SQLite数据库路径不能为空") return errors.New("SQLite数据库路径不能为空")
} }
path := config.Path
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
}
// 检查目录是否存在,不存在则创建 // 检查目录是否存在,不存在则创建
dir := filepath.Dir(config.Path) dir := filepath.Dir(path)
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建SQLite数据库目录失败: %w", err) return fmt.Errorf("创建SQLite数据库目录失败: %w", err)
@@ -160,7 +167,11 @@ func validateLogConfig(config *LogConfig) error {
// 检查日志文件目录(仅当日志文件路径不为空时) // 检查日志文件目录(仅当日志文件路径不为空时)
if config.File != "" { if config.File != "" {
dir := filepath.Dir(config.File) path := config.File
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
}
dir := filepath.Dir(path)
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建日志目录失败: %w", err) return fmt.Errorf("创建日志目录失败: %w", err)

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
@@ -103,7 +104,10 @@ func performInitFromViper() error {
case "sqlite": case "sqlite":
dbPath := cfg.Database.SQLite.Path dbPath := cfg.Database.SQLite.Path
if dbPath == "" { if dbPath == "" {
dbPath = "./database.db" dbPath = "database.db"
}
if !filepath.IsAbs(dbPath) {
dbPath = filepath.Join(utils.GetRootDir(), dbPath)
} }
if _, err := os.Stat(dbPath); os.IsNotExist(err) { if _, err := os.Stat(dbPath); os.IsNotExist(err) {
logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接") logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接")
@@ -209,7 +213,10 @@ func buildGormLogger(level string) gLogger.Interface {
func initSQLite(sqliteConfig *appconfig.SQLiteConfig, logLevel string) error { func initSQLite(sqliteConfig *appconfig.SQLiteConfig, logLevel string) error {
path := sqliteConfig.Path path := sqliteConfig.Path
if path == "" { if path == "" {
path = "./database.db" path = "database.db"
}
if !filepath.IsAbs(path) {
path = filepath.Join(utils.GetRootDir(), path)
} }
dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path) dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path)
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)}) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})

View File

@@ -2,12 +2,14 @@ package server
import ( import (
"NetworkAuth/public" "NetworkAuth/public"
"NetworkAuth/utils"
"io" "io"
"io/fs" "io/fs"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -48,6 +50,9 @@ func registerFrontendRoutes(r *gin.Engine) {
return // 反向代理接管了所有非 API 路由,直接返回 return // 反向代理接管了所有非 API 路由,直接返回
} else { } else {
// 使用本地外部目录 // 使用本地外部目录
if !filepath.IsAbs(distConfig) {
distConfig = filepath.Join(utils.GetRootDir(), distConfig)
}
fileServer = http.FileServer(http.Dir(distConfig)) fileServer = http.FileServer(http.Dir(distConfig))
// 拦截并处理静态资源请求 // 拦截并处理静态资源请求

View File

@@ -1,326 +1,343 @@
package request package request
import ( import (
"bytes" "bytes"
"compress/flate" "compress/flate"
"compress/gzip" "compress/gzip"
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"reflect" "reflect"
"strings" "strings"
"time" "time"
"unsafe" "unsafe"
"github.com/andybalholm/brotli" "github.com/andybalholm/brotli"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/skycheung803/go-bypasser" "github.com/skycheung803/go-bypasser"
) )
type RestyClient struct { type RestyClient struct {
client *resty.Client client *resty.Client
} }
func (request *RestyClient) Resty() *resty.Client { func (request *RestyClient) Resty() *resty.Client {
return request.client return request.client
} }
// NewClient 创建一个基于 uTLS 指纹与 HTTP/2 指纹的 Resty 客户端 // NewClient 创建一个基于 uTLS 指纹与 HTTP/2 指纹的 Resty 客户端
// baseURL 不为空则设置默认 BaseURLproxyStr 不为空则启用 HTTP 代理(仅 HTTP/1.1 // baseURL 不为空则设置默认 BaseURLproxyStr 不为空则启用 HTTP 代理(仅 HTTP/1.1
// persistCookies 启用持久化 CookiefollowRedirect 启用重定向跟随timeout 设置超时时间0 或负数则默认 60 秒) // persistCookies 启用持久化 CookiefollowRedirect 启用重定向跟随timeout 设置超时时间0 或负数则默认 60 秒)
func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int) *RestyClient { func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int) *RestyClient {
rc := resty.New() rc := resty.New()
if baseURL != "" { if baseURL != "" {
rc.SetBaseURL(baseURL) rc.SetBaseURL(baseURL)
} }
if persistCookies { if persistCookies {
jar, _ := cookiejar.New(nil) jar, _ := cookiejar.New(nil)
rc.SetCookieJar(jar) rc.SetCookieJar(jar)
} }
// 设置请求超时时间,如果传入 0 或负数则默认 60 秒 // 设置请求超时时间,如果传入 0 或负数则默认 60 秒
if timeout <= 0 { if timeout <= 0 {
timeout = 60 timeout = 60
} }
rc.SetTimeout(time.Duration(timeout) * time.Second) rc.SetTimeout(time.Duration(timeout) * time.Second)
// 统一设置客户端默认请求头(调用级 headers 可覆盖),字段按字母顺序排列 // 统一设置客户端默认请求头(调用级 headers 可覆盖),字段按字母顺序排列
rc.SetHeader("accept", "*/*") rc.SetHeader("accept", "*/*")
rc.SetHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") rc.SetHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
rc.SetHeader("connection", "keep-alive") rc.SetHeader("connection", "keep-alive")
rc.SetHeader("pragma", "no-cache") rc.SetHeader("pragma", "no-cache")
rc.SetHeader("priority", "u=1,i") rc.SetHeader("priority", "u=1,i")
rc.SetHeader("sec-ch-ua", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"") rc.SetHeader("sec-ch-ua", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"")
rc.SetHeader("sec-ch-ua-mobile", "?0") rc.SetHeader("sec-ch-ua-mobile", "?0")
rc.SetHeader("sec-ch-ua-platform", "\"macOS\"") rc.SetHeader("sec-ch-ua-platform", "\"macOS\"")
rc.SetHeader("sec-fetch-dest", "empty") rc.SetHeader("sec-fetch-dest", "empty")
rc.SetHeader("sec-fetch-mode", "cors") rc.SetHeader("sec-fetch-mode", "cors")
rc.SetHeader("sec-fetch-site", "same-origin") rc.SetHeader("sec-fetch-site", "same-origin")
rc.SetHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36") rc.SetHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
// 初始化 go-bypasser 替代原有的 spoofed-round-tripper // 初始化 go-bypasser 替代原有的 spoofed-round-tripper
opts := []bypasser.BypasserOption{ opts := []bypasser.BypasserOption{
bypasser.WithInsecureSkipVerify(true), bypasser.WithInsecureSkipVerify(true),
} }
if proxyStr != "" { if proxyStr != "" {
opts = append(opts, bypasser.WithProxy(proxyStr)) opts = append(opts, bypasser.WithProxy(proxyStr))
} }
bypass, err := bypasser.NewBypasser(opts...) bypass, err := bypasser.NewBypasser(opts...)
if err != nil { if err != nil {
panic(err) panic(err)
} }
rc.SetTransport(bypass.Transport) rc.SetTransport(&sanitizeTransport{t: bypass.Transport})
return &RestyClient{client: rc} return &RestyClient{client: rc}
} }
// fillResponseBody 使用反射强制填充响应体 // sanitizeTransport 包装 http.RoundTripper 以修复底层库可能违背 Go 接口约定的行为
// 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body type sanitizeTransport struct {
// 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中 t http.RoundTripper
func (request *RestyClient) fillResponseBody(resp *resty.Response) { }
if resp == nil || resp.RawResponse == nil {
return func (s *sanitizeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
} resp, err := s.t.RoundTrip(req)
// 如果已经有 body 内容,则不处理 // net/http 规定 RoundTripper 要么返回有效的 resp 和 nil error要么返回 nil resp 和有效的 error。
if len(resp.Body()) > 0 { // 某些第三方库(如部分 tls-client 封装)在遇到网络小问题时会同时返回 resp 和 err。
return // 这会导致 net/http 打印 "RoundTripper returned a response & error; ignoring response" 并强制丢弃响应。
} // 在这里我们进行修正:如果已经拿到了响应(哪怕是不完整的),我们优先保留响应并将 err 置空,让上层通过读取 Body 自行发现错误。
if resp != nil && err != nil {
// 读取底层 Body err = nil
bodyBytes, err := io.ReadAll(resp.RawResponse.Body) }
if err != nil { return resp, err
return }
}
resp.RawResponse.Body.Close() // fillResponseBody 使用反射强制填充响应体
// 重置 Body 以便后续可能得读取 // 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body
resp.RawResponse.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中
func (request *RestyClient) fillResponseBody(resp *resty.Response) {
// 使用反射设置私有字段 body if resp == nil || resp.RawResponse == nil {
v := reflect.ValueOf(resp).Elem() return
f := v.FieldByName("body") }
if f.IsValid() { // 如果已经有 body 内容,则不处理
// 必须使用 UnsafeAddr 获取未导出字段的地址 if len(resp.Body()) > 0 {
rf := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem() return
rf.SetBytes(bodyBytes) }
}
// 读取底层 Body
// 设置 size 字段 bodyBytes, err := io.ReadAll(resp.RawResponse.Body)
s := v.FieldByName("size") if err != nil {
if s.IsValid() { return
rs := reflect.NewAt(s.Type(), unsafe.Pointer(s.UnsafeAddr())).Elem() }
rs.SetInt(int64(len(bodyBytes))) resp.RawResponse.Body.Close()
} // 重置 Body 以便后续可能得读取
} resp.RawResponse.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// makeReq 构造带可选请求头的 resty.Request // 使用反射设置私有字段 body
// 功能:基于客户端创建请求对象,并在传入 headers 时进行设置 v := reflect.ValueOf(resp).Elem()
// 返回:带有请求头的请求对象 f := v.FieldByName("body")
func (request *RestyClient) makeReq(headers map[string]string, cookies []*http.Cookie) *resty.Request { if f.IsValid() {
req := request.client.R() // 必须使用 UnsafeAddr 获取未导出字段的地址
if len(headers) > 0 { rf := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem()
req = req.SetHeaders(headers) rf.SetBytes(bodyBytes)
} }
if len(cookies) > 0 {
req = req.SetCookies(cookies) // 设置 size 字段
} s := v.FieldByName("size")
return req if s.IsValid() {
} rs := reflect.NewAt(s.Type(), unsafe.Pointer(s.UnsafeAddr())).Elem()
rs.SetInt(int64(len(bodyBytes)))
// doWithEncodingFallback 封装请求发送并在出现压缩相关错误时进行一次降级重试 }
// 逻辑:首次请求失败且错误包含 gzip/zstd/brotli/magic number mismatch 时,设置 accept-encoding 为 identity 重试一次 }
func (request *RestyClient) doWithEncodingFallback(headers map[string]string, cookies []*http.Cookie, allowRedirect bool, do func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
req := request.makeReq(headers, cookies) // makeReq 构造带可选请求头的 resty.Request
if allowRedirect { // 功能:基于客户端创建请求对象,并在传入 headers 时进行设置
request.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10)) // 返回:带有请求头的请求对象
} else { func (request *RestyClient) makeReq(headers map[string]string, cookies []*http.Cookie) *resty.Request {
// 使用 http.ErrUseLastResponse 确保 302 响应被返回且 Body 可读,而不是报错 req := request.client.R()
request.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { if len(headers) > 0 {
return http.ErrUseLastResponse req = req.SetHeaders(headers)
})) }
} if len(cookies) > 0 {
resp, err := do(req) req = req.SetCookies(cookies)
}
// 尝试补救响应体(特别是当重定向被禁用导致报错时) return req
request.fillResponseBody(resp) }
if err == nil { // doWithEncodingFallback 封装请求发送并在出现压缩相关错误时进行一次降级重试
return resp, nil // 逻辑:首次请求失败且错误包含 gzip/zstd/brotli/magic number mismatch 时,设置 accept-encoding 为 identity 重试一次
} func (request *RestyClient) doWithEncodingFallback(headers map[string]string, cookies []*http.Cookie, allowRedirect bool, do func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
s := err.Error() req := request.makeReq(headers, cookies)
if strings.Contains(s, "gzip: invalid header") || strings.Contains(s, "magic number mismatch") || strings.Contains(s, "zstd") || strings.Contains(s, "brotli") { if allowRedirect {
h2 := map[string]string{} request.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
for k, v := range headers { } else {
if strings.ToLower(k) != "accept-encoding" { // 使用 http.ErrUseLastResponse 确保 302 响应被返回且 Body 可读,而不是报错
h2[k] = v request.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
} return http.ErrUseLastResponse
} }))
h2["Accept-Encoding"] = "identity" }
req2 := request.makeReq(h2, cookies) resp, err := do(req)
resp2, err2 := do(req2)
request.fillResponseBody(resp2) // 尝试补救响应体(特别是当重定向被禁用导致报错时)
if err2 == nil { request.fillResponseBody(resp)
return resp2, nil
} if err == nil {
} return resp, nil
return resp, err }
} s := err.Error()
if strings.Contains(s, "gzip: invalid header") || strings.Contains(s, "magic number mismatch") || strings.Contains(s, "zstd") || strings.Contains(s, "brotli") {
// decodeResponse 处理响应解压与 JSON 解析 h2 := map[string]string{}
// 功能:自动识别 gzip 压缩并解压;在 result 非空时按 JSON 解析到 result for k, v := range headers {
// 返回:解析错误(成功时为 nil if strings.ToLower(k) != "accept-encoding" {
func (request *RestyClient) decodeResponse(resp *resty.Response, result interface{}) error { h2[k] = v
if resp == nil { }
return nil }
} h2["Accept-Encoding"] = "identity"
ct := strings.ToLower(resp.Header().Get("Content-Type")) req2 := request.makeReq(h2, cookies)
ce := strings.ToLower(resp.Header().Get("Content-Encoding")) resp2, err2 := do(req2)
body := resp.Body() request.fillResponseBody(resp2)
if strings.Contains(ce, "gzip") && len(body) > 0 { if err2 == nil {
gr, gerr := gzip.NewReader(bytes.NewReader(body)) return resp2, nil
if gerr == nil { }
defer gr.Close() }
if dec, derr := io.ReadAll(gr); derr == nil { return resp, err
body = dec }
resp.SetBody(body)
} // decodeResponse 处理响应解压与 JSON 解析
} // 功能:自动识别 gzip 压缩并解压;在 result 非空时按 JSON 解析到 result
} else if strings.Contains(ce, "deflate") && len(body) > 0 { // 返回:解析错误(成功时为 nil
// 处理 deflate 压缩 func (request *RestyClient) decodeResponse(resp *resty.Response, result interface{}) error {
dr := flate.NewReader(bytes.NewReader(body)) if resp == nil {
defer dr.Close() return nil
if dec, derr := io.ReadAll(dr); derr == nil { }
body = dec ct := strings.ToLower(resp.Header().Get("Content-Type"))
resp.SetBody(body) ce := strings.ToLower(resp.Header().Get("Content-Encoding"))
} body := resp.Body()
} else if strings.Contains(ce, "br") && len(body) > 0 { if strings.Contains(ce, "gzip") && len(body) > 0 {
// 处理 brotli 压缩 gr, gerr := gzip.NewReader(bytes.NewReader(body))
br := brotli.NewReader(bytes.NewReader(body)) if gerr == nil {
if dec, derr := io.ReadAll(br); derr == nil { defer gr.Close()
body = dec if dec, derr := io.ReadAll(gr); derr == nil {
resp.SetBody(body) // 将解压后的 body 写回 response body = dec
} resp.SetBody(body)
} }
if result != nil && (strings.Contains(ct, "application/json") || json.Valid(body)) { }
if err := json.Unmarshal(body, result); err != nil { } else if strings.Contains(ce, "deflate") && len(body) > 0 {
return err // 处理 deflate 压缩
} dr := flate.NewReader(bytes.NewReader(body))
} defer dr.Close()
return nil if dec, derr := io.ReadAll(dr); derr == nil {
} body = dec
resp.SetBody(body)
// RestyGet 发送 GET 请求 }
func (request *RestyClient) RestyGet(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) { } else if strings.Contains(ce, "br") && len(body) > 0 {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { // 处理 brotli 压缩
return r.Get(path) br := brotli.NewReader(bytes.NewReader(body))
}) if dec, derr := io.ReadAll(br); derr == nil {
if resp == nil && err != nil { body = dec
return nil, err resp.SetBody(body) // 将解压后的 body 写回 response
} }
}
if err := request.decodeResponse(resp, result); err != nil { if result != nil && (strings.Contains(ct, "application/json") || json.Valid(body)) {
return nil, err if err := json.Unmarshal(body, result); err != nil {
} return err
}
return resp, err }
} return nil
}
// RestyPost 发送 POST 请求
func (request *RestyClient) RestyPost(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) { // RestyGet 发送 GET 请求
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { func (request *RestyClient) RestyGet(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
return r.SetBody(data).Post(path) resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
}) return r.Get(path)
if resp == nil && err != nil { })
return nil, err if resp == nil && err != nil {
} return nil, err
}
if err := request.decodeResponse(resp, result); err != nil {
return nil, err if err := request.decodeResponse(resp, result); err != nil {
} return nil, err
}
return resp, err
} return resp, err
}
// RestyPut 发送 PUT 请求
// 功能:发送 PUT支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON // RestyPost 发送 POST 请求
// 返回:响应对象与错误信息 func (request *RestyClient) RestyPost(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
func (request *RestyClient) RestyPut(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) { resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { return r.SetBody(data).Post(path)
return r.SetBody(data).Put(path) })
}) if resp == nil && err != nil {
if resp == nil && err != nil { return nil, err
return nil, err }
}
if err := request.decodeResponse(resp, result); err != nil {
if err := request.decodeResponse(resp, result); err != nil { return nil, err
return nil, err }
}
return resp, err
return resp, err }
}
// RestyPut 发送 PUT 请求
// RestyPatch 发送 PATCH 请求 // 功能:发送 PUT支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
// 功能:发送 PATCH支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON // 返回:响应对象与错误信息
// 返回:响应对象与错误信息 func (request *RestyClient) RestyPut(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
func (request *RestyClient) RestyPatch(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) { resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { return r.SetBody(data).Put(path)
return r.SetBody(data).Patch(path) })
}) if resp == nil && err != nil {
if resp == nil && err != nil { return nil, err
return nil, err }
}
if err := request.decodeResponse(resp, result); err != nil {
if err := request.decodeResponse(resp, result); err != nil { return nil, err
return nil, err }
}
return resp, err
return resp, err }
}
// RestyPatch 发送 PATCH 请求
// RestyDelete 发送 DELETE 请求 // 功能:发送 PATCH支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
// 功能:发送 DELETE支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON // 返回:响应对象与错误信息
// 返回:响应对象与错误信息 func (request *RestyClient) RestyPatch(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
func (request *RestyClient) RestyDelete(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) { resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { return r.SetBody(data).Patch(path)
return r.Delete(path) })
}) if resp == nil && err != nil {
if resp == nil && err != nil { return nil, err
return nil, err }
}
if err := request.decodeResponse(resp, result); err != nil {
if err := request.decodeResponse(resp, result); err != nil { return nil, err
return nil, err }
}
return resp, err
return resp, err }
}
// RestyDelete 发送 DELETE 请求
// RestyHead 发送 HEAD 请求 // 功能:发送 DELETE支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
// 功能:发送 HEAD支持请求级 headers 覆盖客户端默认HEAD 通常无正文 // 返回:响应对象与错误信息
// 返回:响应对象与错误信息 func (request *RestyClient) RestyDelete(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
func (request *RestyClient) RestyHead(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) { resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { return r.Delete(path)
return r.Head(path) })
}) if resp == nil && err != nil {
if resp == nil && err != nil { return nil, err
return nil, err }
}
return resp, err if err := request.decodeResponse(resp, result); err != nil {
} return nil, err
}
// RestyOptions 发送 OPTIONS 请求
// 功能:发送 OPTIONS支持请求级 headers 覆盖客户端默认 return resp, err
// 返回:响应对象与错误信息 }
func (request *RestyClient) RestyOptions(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) { // RestyHead 发送 HEAD 请求
return r.Options(path) // 功能:发送 HEAD支持请求级 headers 覆盖客户端默认HEAD 通常无正文
}) // 返回:响应对象与错误信息
if resp == nil && err != nil { func (request *RestyClient) RestyHead(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
return nil, err resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
} return r.Head(path)
return resp, err })
} if resp == nil && err != nil {
return nil, err
}
return resp, err
}
// RestyOptions 发送 OPTIONS 请求
// 功能:发送 OPTIONS支持请求级 headers 覆盖客户端默认
// 返回:响应对象与错误信息
func (request *RestyClient) RestyOptions(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.Options(path)
})
if resp == nil && err != nil {
return nil, err
}
return resp, err
}

64
utils/path.go Normal file
View File

@@ -0,0 +1,64 @@
package utils
import (
"os"
"path/filepath"
"strings"
)
// GetRootDir 获取当前程序运行的真实根目录
// 能够智能、跨平台地识别是编译后的可执行文件运行,还是通过 `go run` 运行(通常在临时目录下)
func GetRootDir() string {
var baseDir string
// 首先尝试获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
workDir = "."
}
// 获取程序可执行文件所在目录
execPath, err := os.Executable()
if err != nil {
// 如果获取可执行文件路径失败,使用当前工作目录
return workDir
}
// 解析软链接获取真实物理路径macOS 下 /tmp 经常是 /private/tmp 的软链)
realExecPath, err := filepath.EvalSymlinks(execPath)
if err == nil {
execPath = realExecPath
}
execDir := filepath.Dir(execPath)
realTempDir, err := filepath.EvalSymlinks(os.TempDir())
if err != nil {
realTempDir = os.TempDir()
}
// 跨平台安全地判断 execDir 是否在 realTempDir 内部
// 使用 filepath.Rel 可以避免直接 HasPrefix 带来的大小写、路径分隔符以及部分目录名重合的问题
rel, err := filepath.Rel(realTempDir, execDir)
isGoRun := false
if err == nil {
// 如果 rel 不以 ".." 开头,说明 execDir 在 TempDir 内部,即为 go run 模式
if rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
isGoRun = true
}
} else {
// fallback: 如果 Rel 失败(例如跨盘符),则退回简单的 HasPrefix 判断(带上分隔符防误判)
cleanTemp := filepath.Clean(realTempDir) + string(os.PathSeparator)
cleanExec := filepath.Clean(execDir) + string(os.PathSeparator)
if strings.HasPrefix(strings.ToLower(cleanExec), strings.ToLower(cleanTemp)) {
isGoRun = true
}
}
if isGoRun {
baseDir = workDir
} else {
baseDir = execDir
}
return baseDir
}