mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
增加 自定义导航栏模块
This commit is contained in:
80
services/portal_navigation.go
Normal file
80
services/portal_navigation.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"NetworkAuth/models"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const portalNavigationAdminPath = "admin"
|
||||
const portalNavigationAdminSort = 999
|
||||
|
||||
// NormalizePortalNavigation 规范化门户导航数据
|
||||
// 统一清理首尾空白,并处理首页与排序约束
|
||||
func NormalizePortalNavigation(item *models.PortalNavigation) {
|
||||
item.Name = strings.TrimSpace(item.Name)
|
||||
item.Path = strings.TrimSpace(item.Path)
|
||||
if item.Sort < 0 {
|
||||
item.Sort = 0
|
||||
}
|
||||
if item.IsHome {
|
||||
item.IsHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
// IsPortalNavigationAdminEntry 判断是否为管理员入口
|
||||
// 管理员入口属于系统保留导航项,不允许修改基础信息
|
||||
func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(item.Path), portalNavigationAdminPath)
|
||||
}
|
||||
|
||||
// LockPortalNavigationProtectedFields 锁定系统保留导航字段
|
||||
// 管理员入口仅允许调整隐藏状态,其余字段保持系统固定值
|
||||
func LockPortalNavigationProtectedFields(item *models.PortalNavigation, exists models.PortalNavigation) {
|
||||
switch IsPortalNavigationAdminEntry(exists) {
|
||||
case true:
|
||||
item.Name = "管理员登录"
|
||||
item.Path = portalNavigationAdminPath
|
||||
item.Sort = portalNavigationAdminSort
|
||||
item.IsHome = false
|
||||
item.IsExternal = false
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SavePortalNavigation 保存门户导航
|
||||
// 当当前记录被设置为门户首页时,会自动取消其他记录的首页状态
|
||||
func SavePortalNavigation(db *gorm.DB, item *models.PortalNavigation, exists ...models.PortalNavigation) error {
|
||||
if len(exists) > 0 {
|
||||
LockPortalNavigationProtectedFields(item, exists[0])
|
||||
}
|
||||
NormalizePortalNavigation(item)
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if item.IsHome {
|
||||
query := tx.Model(&models.PortalNavigation{}).Where("is_home = ?", true)
|
||||
if item.ID > 0 {
|
||||
query = query.Where("id <> ?", item.ID)
|
||||
}
|
||||
if err := query.Update("is_home", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case item.ID == 0:
|
||||
return tx.Create(item).Error
|
||||
default:
|
||||
return tx.Model(&models.PortalNavigation{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"name": item.Name,
|
||||
"path": item.Path,
|
||||
"sort": item.Sort,
|
||||
"is_home": item.IsHome,
|
||||
"is_hidden": item.IsHidden,
|
||||
"is_external": item.IsExternal,
|
||||
}).Error
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,64 +4,119 @@ import (
|
||||
"bytes"
|
||||
"compress/flate"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"reflect"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/go-resty/resty/v2"
|
||||
req "github.com/imroc/req/v3"
|
||||
"github.com/skycheung803/go-bypasser"
|
||||
)
|
||||
|
||||
type RestyClient struct {
|
||||
client *resty.Client
|
||||
client *resty.Client
|
||||
reqClient *req.Client
|
||||
ctx context.Context
|
||||
baseURL string
|
||||
defaultHeaders map[string]string
|
||||
proxyStr string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (request *RestyClient) Resty() *resty.Client {
|
||||
return request.client
|
||||
}
|
||||
|
||||
// NewClient 创建一个基于 uTLS 指纹与 HTTP/2 指纹的 Resty 客户端
|
||||
// baseURL 不为空则设置默认 BaseURL;proxyStr 不为空则启用 HTTP 代理(仅 HTTP/1.1)
|
||||
// persistCookies 启用持久化 Cookie;followRedirect 启用重定向跟随;timeout 设置超时时间(秒,0 或负数则默认 60 秒)
|
||||
// NewClient 创建一个基于 go-bypasser(req/v3) 的客户端。
|
||||
// 对外继续保留 Resty 风格接口,但底层请求不再走 resty.Transport。
|
||||
func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int) *RestyClient {
|
||||
rc := resty.New()
|
||||
|
||||
if baseURL != "" {
|
||||
rc.SetBaseURL(baseURL)
|
||||
}
|
||||
|
||||
if persistCookies {
|
||||
jar, _ := cookiejar.New(nil)
|
||||
rc.SetCookieJar(jar)
|
||||
}
|
||||
|
||||
// 设置请求超时时间,如果传入 0 或负数则默认 60 秒
|
||||
if timeout <= 0 {
|
||||
timeout = 60
|
||||
}
|
||||
rc.SetTimeout(time.Duration(timeout) * time.Second)
|
||||
timeoutDuration := time.Duration(timeout) * time.Second
|
||||
|
||||
// 统一设置客户端默认请求头(调用级 headers 可覆盖),字段按字母顺序排列
|
||||
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("connection", "keep-alive")
|
||||
rc.SetHeader("pragma", "no-cache")
|
||||
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-mobile", "?0")
|
||||
rc.SetHeader("sec-ch-ua-platform", "\"macOS\"")
|
||||
rc.SetHeader("sec-fetch-dest", "empty")
|
||||
rc.SetHeader("sec-fetch-mode", "cors")
|
||||
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")
|
||||
defaultHeaders := map[string]string{
|
||||
"accept": "*/*",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"connection": "keep-alive",
|
||||
"pragma": "no-cache",
|
||||
"priority": "u=1,i",
|
||||
"sec-ch-ua": "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"",
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": "\"macOS\"",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"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
|
||||
stateClient := resty.New().
|
||||
SetTimeout(timeoutDuration).
|
||||
SetHeaders(defaultHeaders)
|
||||
if baseURL != "" {
|
||||
stateClient.SetBaseURL(baseURL)
|
||||
}
|
||||
|
||||
var sharedJar http.CookieJar
|
||||
if persistCookies {
|
||||
jar, _ := cookiejar.New(nil)
|
||||
sharedJar = jar
|
||||
stateClient.SetCookieJar(sharedJar)
|
||||
}
|
||||
|
||||
baseReqClient := mustNewReqClient(proxyStr, timeoutDuration, baseURL, defaultHeaders, sharedJar)
|
||||
|
||||
return &RestyClient{
|
||||
client: stateClient,
|
||||
reqClient: baseReqClient,
|
||||
ctx: context.Background(),
|
||||
baseURL: baseURL,
|
||||
defaultHeaders: defaultHeaders,
|
||||
proxyStr: proxyStr,
|
||||
timeout: timeoutDuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (request *RestyClient) WithContext(ctx context.Context) *RestyClient {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return &RestyClient{
|
||||
client: request.client,
|
||||
reqClient: request.reqClient,
|
||||
ctx: ctx,
|
||||
baseURL: request.baseURL,
|
||||
defaultHeaders: request.defaultHeaders,
|
||||
proxyStr: request.proxyStr,
|
||||
timeout: request.timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// SetPersistentHeader 设置持久化 Header。
|
||||
// 除 Cookie 外,其余 Header 会同步到 req 客户端的 common headers。
|
||||
func (request *RestyClient) SetPersistentHeader(key string, value string) {
|
||||
if request.defaultHeaders == nil {
|
||||
request.defaultHeaders = make(map[string]string)
|
||||
}
|
||||
lowerKey := strings.ToLower(key)
|
||||
request.defaultHeaders[lowerKey] = value
|
||||
if request.client != nil {
|
||||
request.client.SetHeader(key, value)
|
||||
}
|
||||
if request.reqClient != nil && lowerKey != "cookie" {
|
||||
request.reqClient.SetCommonHeader(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func mustNewReqClient(proxyStr string, timeout time.Duration, baseURL string, defaultHeaders map[string]string, jar http.CookieJar) *req.Client {
|
||||
opts := []bypasser.BypasserOption{
|
||||
bypasser.WithInsecureSkipVerify(true),
|
||||
}
|
||||
@@ -74,155 +129,287 @@ func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rc.SetTransport(&sanitizeTransport{t: bypass.Transport})
|
||||
|
||||
return &RestyClient{client: rc}
|
||||
}
|
||||
|
||||
// sanitizeTransport 包装 http.RoundTripper 以修复底层库可能违背 Go 接口约定的行为
|
||||
type sanitizeTransport struct {
|
||||
t http.RoundTripper
|
||||
}
|
||||
|
||||
func (s *sanitizeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
resp, err := s.t.RoundTrip(req)
|
||||
// net/http 规定 RoundTripper 要么返回有效的 resp 和 nil error,要么返回 nil resp 和有效的 error。
|
||||
// 某些第三方库(如部分 tls-client 封装)在遇到网络小问题时会同时返回 resp 和 err。
|
||||
// 这会导致 net/http 打印 "RoundTripper returned a response & error; ignoring response" 并强制丢弃响应。
|
||||
// 在这里我们进行修正:如果已经拿到了响应(哪怕是不完整的),我们优先保留响应并将 err 置空,让上层通过读取 Body 自行发现错误。
|
||||
if resp != nil && err != nil {
|
||||
err = nil
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// fillResponseBody 使用反射强制填充响应体
|
||||
// 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body
|
||||
// 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中
|
||||
func (request *RestyClient) fillResponseBody(resp *resty.Response) {
|
||||
if resp == nil || resp.RawResponse == nil {
|
||||
return
|
||||
}
|
||||
// 如果已经有 body 内容,则不处理
|
||||
if len(resp.Body()) > 0 {
|
||||
return
|
||||
rt, ok := bypass.Transport.(*bypasser.StandardRoundTripper)
|
||||
if !ok || rt.Client == nil {
|
||||
panic("go-bypasser did not return a StandardRoundTripper client")
|
||||
}
|
||||
|
||||
// 读取底层 Body
|
||||
bodyBytes, err := io.ReadAll(resp.RawResponse.Body)
|
||||
client := rt.Client
|
||||
client.SetTimeout(timeout)
|
||||
client.SetRedirectPolicy(req.DefaultRedirectPolicy())
|
||||
if baseURL != "" {
|
||||
client.SetBaseURL(baseURL)
|
||||
}
|
||||
for k, v := range defaultHeaders {
|
||||
if strings.ToLower(k) == "cookie" {
|
||||
continue
|
||||
}
|
||||
client.SetCommonHeader(k, v)
|
||||
}
|
||||
if jar != nil {
|
||||
client.SetCookieJar(jar)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (request *RestyClient) newRequestClient(redirectCount int) *req.Client {
|
||||
client := request.reqClient.Clone()
|
||||
if request.baseURL != "" {
|
||||
client.SetBaseURL(request.baseURL)
|
||||
}
|
||||
client.SetTimeout(request.timeout)
|
||||
|
||||
switch {
|
||||
case redirectCount == 0:
|
||||
client.SetRedirectPolicy(req.NoRedirectPolicy())
|
||||
case redirectCount > 0:
|
||||
client.SetRedirectPolicy(req.MaxRedirectPolicy(redirectCount))
|
||||
default:
|
||||
client.SetRedirectPolicy(req.DefaultRedirectPolicy())
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (request *RestyClient) resolveRequestURL(path string) *url.URL {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
resp.RawResponse.Body.Close()
|
||||
// 重置 Body 以便后续可能得读取
|
||||
resp.RawResponse.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
// 使用反射设置私有字段 body
|
||||
v := reflect.ValueOf(resp).Elem()
|
||||
f := v.FieldByName("body")
|
||||
if f.IsValid() {
|
||||
// 必须使用 UnsafeAddr 获取未导出字段的地址
|
||||
rf := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem()
|
||||
rf.SetBytes(bodyBytes)
|
||||
if parsedURL.IsAbs() {
|
||||
return parsedURL
|
||||
}
|
||||
if request.baseURL == "" {
|
||||
return parsedURL
|
||||
}
|
||||
|
||||
// 设置 size 字段
|
||||
s := v.FieldByName("size")
|
||||
if s.IsValid() {
|
||||
rs := reflect.NewAt(s.Type(), unsafe.Pointer(s.UnsafeAddr())).Elem()
|
||||
rs.SetInt(int64(len(bodyBytes)))
|
||||
baseURL, err := url.Parse(request.baseURL)
|
||||
if err != nil {
|
||||
return parsedURL
|
||||
}
|
||||
return baseURL.ResolveReference(parsedURL)
|
||||
}
|
||||
|
||||
func cloneCookie(cookie *http.Cookie) *http.Cookie {
|
||||
if cookie == nil {
|
||||
return nil
|
||||
}
|
||||
copied := *cookie
|
||||
return &copied
|
||||
}
|
||||
|
||||
func isSafeHTTPCookieValue(value string) bool {
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < 0x21 || r > 0x7e {
|
||||
return false
|
||||
}
|
||||
switch r {
|
||||
case '"', ';', '\\', ',':
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseRawCookieHeader(raw string) []*http.Cookie {
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var cookies []*http.Cookie
|
||||
for _, part := range strings.Split(raw, ";") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
name, value, ok := strings.Cut(part, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
value = strings.TrimSpace(value)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
cookies = append(cookies, &http.Cookie{Name: name, Value: value})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
|
||||
func buildCookieHeader(cookies []*http.Cookie) string {
|
||||
if len(cookies) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(cookies))
|
||||
for _, cookie := range cookies {
|
||||
if cookie == nil || cookie.Name == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value))
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// decodeCompressedBody 按响应头解压正文,兼容项目里依赖明文 HTML/JSON 的解析逻辑。
|
||||
func decodeCompressedBody(body []byte, contentEncoding string) ([]byte, error) {
|
||||
encoding := strings.ToLower(strings.TrimSpace(contentEncoding))
|
||||
switch {
|
||||
case encoding == "", encoding == "identity":
|
||||
return body, nil
|
||||
case strings.Contains(encoding, "gzip"):
|
||||
reader, err := gzip.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
return io.ReadAll(reader)
|
||||
case strings.Contains(encoding, "deflate"):
|
||||
reader := flate.NewReader(bytes.NewReader(body))
|
||||
defer reader.Close()
|
||||
return io.ReadAll(reader)
|
||||
case strings.Contains(encoding, "br"):
|
||||
reader := brotli.NewReader(bytes.NewReader(body))
|
||||
return io.ReadAll(reader)
|
||||
default:
|
||||
return body, nil
|
||||
}
|
||||
}
|
||||
|
||||
// makeReq 构造带可选请求头的 resty.Request
|
||||
// 功能:基于客户端创建请求对象,并在传入 headers 时进行设置
|
||||
// 返回:带有请求头的请求对象
|
||||
func (request *RestyClient) makeReq(headers map[string]string, cookies []*http.Cookie) *resty.Request {
|
||||
req := request.client.R()
|
||||
func (request *RestyClient) prepareCookies(path string, requestCookies []*http.Cookie) ([]*http.Cookie, string) {
|
||||
cookieMap := make(map[string]*http.Cookie)
|
||||
order := make([]string, 0)
|
||||
rawCookieNames := make(map[string]struct{})
|
||||
|
||||
appendCookie := func(cookie *http.Cookie) {
|
||||
if cookie == nil || cookie.Name == "" {
|
||||
return
|
||||
}
|
||||
if _, exists := cookieMap[cookie.Name]; !exists {
|
||||
order = append(order, cookie.Name)
|
||||
}
|
||||
cloned := cloneCookie(cookie)
|
||||
cookieMap[cookie.Name] = cloned
|
||||
if cloned != nil && !isSafeHTTPCookieValue(cloned.Value) {
|
||||
rawCookieNames[cloned.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
parsedURL := request.resolveRequestURL(path)
|
||||
if request.client != nil && request.client.GetClient() != nil && request.client.GetClient().Jar != nil && parsedURL != nil {
|
||||
for _, cookie := range request.client.GetClient().Jar.Cookies(parsedURL) {
|
||||
appendCookie(cookie)
|
||||
}
|
||||
}
|
||||
if request.client != nil {
|
||||
for _, cookie := range request.client.Cookies {
|
||||
appendCookie(cookie)
|
||||
}
|
||||
}
|
||||
for _, cookie := range requestCookies {
|
||||
appendCookie(cookie)
|
||||
}
|
||||
|
||||
rawCookies := parseRawCookieHeader(request.defaultHeaders["cookie"])
|
||||
for _, cookie := range rawCookies {
|
||||
if cookie == nil || cookie.Name == "" {
|
||||
continue
|
||||
}
|
||||
rawCookieNames[cookie.Name] = struct{}{}
|
||||
if _, exists := cookieMap[cookie.Name]; !exists {
|
||||
order = append(order, cookie.Name)
|
||||
}
|
||||
cookieMap[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
mergedCookies := make([]*http.Cookie, 0, len(order))
|
||||
for _, name := range order {
|
||||
if cookie := cookieMap[name]; cookie != nil {
|
||||
mergedCookies = append(mergedCookies, cloneCookie(cookie))
|
||||
}
|
||||
}
|
||||
|
||||
if len(rawCookieNames) == 0 {
|
||||
return mergedCookies, ""
|
||||
}
|
||||
return mergedCookies, buildCookieHeader(mergedCookies)
|
||||
}
|
||||
|
||||
func (request *RestyClient) buildReqRequest(client *req.Client, path string, headers map[string]string, cookies []*http.Cookie) *req.Request {
|
||||
r := client.R().SetContext(request.ctx)
|
||||
if len(headers) > 0 {
|
||||
req = req.SetHeaders(headers)
|
||||
r.SetHeaders(headers)
|
||||
}
|
||||
if len(cookies) > 0 {
|
||||
req = req.SetCookies(cookies)
|
||||
|
||||
mergedCookies, rawCookieHeader := request.prepareCookies(path, cookies)
|
||||
if rawCookieHeader != "" {
|
||||
r.SetHeader("Cookie", rawCookieHeader)
|
||||
} else if len(mergedCookies) > 0 {
|
||||
r.SetCookies(mergedCookies...)
|
||||
}
|
||||
return req
|
||||
return r
|
||||
}
|
||||
|
||||
// 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)
|
||||
if allowRedirect {
|
||||
request.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
|
||||
} else {
|
||||
// 使用 http.ErrUseLastResponse 确保 302 响应被返回且 Body 可读,而不是报错
|
||||
request.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}))
|
||||
}
|
||||
resp, err := do(req)
|
||||
|
||||
// 尝试补救响应体(特别是当重定向被禁用导致报错时)
|
||||
request.fillResponseBody(resp)
|
||||
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
s := err.Error()
|
||||
if strings.Contains(s, "gzip: invalid header") || strings.Contains(s, "magic number mismatch") || strings.Contains(s, "zstd") || strings.Contains(s, "brotli") {
|
||||
h2 := map[string]string{}
|
||||
for k, v := range headers {
|
||||
if strings.ToLower(k) != "accept-encoding" {
|
||||
h2[k] = v
|
||||
}
|
||||
}
|
||||
h2["Accept-Encoding"] = "identity"
|
||||
req2 := request.makeReq(h2, cookies)
|
||||
resp2, err2 := do(req2)
|
||||
request.fillResponseBody(resp2)
|
||||
if err2 == nil {
|
||||
return resp2, nil
|
||||
}
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// decodeResponse 处理响应解压与 JSON 解析
|
||||
// 功能:自动识别 gzip 压缩并解压;在 result 非空时按 JSON 解析到 result
|
||||
// 返回:解析错误(成功时为 nil)
|
||||
func (request *RestyClient) decodeResponse(resp *resty.Response, result interface{}) error {
|
||||
func (request *RestyClient) adaptReqResponse(path string, method string, data any, headers map[string]string, cookies []*http.Cookie, resp *req.Response) (*resty.Response, error) {
|
||||
if resp == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
body, err := resp.ToBytes()
|
||||
if err != nil && resp.Response == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawResponse := resp.Response
|
||||
if rawResponse != nil {
|
||||
decodedBody, decodeErr := decodeCompressedBody(body, rawResponse.Header.Get("Content-Encoding"))
|
||||
if decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
body = decodedBody
|
||||
rawResponse.Body = io.NopCloser(bytes.NewReader(body))
|
||||
rawResponse.Header.Del("Content-Encoding")
|
||||
rawResponse.ContentLength = int64(len(body))
|
||||
}
|
||||
|
||||
restyReq := request.client.R()
|
||||
restyReq.Method = method
|
||||
restyReq.URL = path
|
||||
restyReq.Body = data
|
||||
restyReq.Header = make(http.Header)
|
||||
for k, v := range headers {
|
||||
restyReq.Header.Set(k, v)
|
||||
}
|
||||
mergedCookies, rawCookieHeader := request.prepareCookies(path, cookies)
|
||||
if rawCookieHeader != "" {
|
||||
restyReq.Header.Set("Cookie", rawCookieHeader)
|
||||
} else {
|
||||
restyReq.Cookies = mergedCookies
|
||||
}
|
||||
|
||||
restyResp := &resty.Response{
|
||||
Request: restyReq,
|
||||
RawResponse: rawResponse,
|
||||
}
|
||||
restyResp.SetBody(body)
|
||||
return restyResp, err
|
||||
}
|
||||
|
||||
func (request *RestyClient) decodeResponse(resp *resty.Response, result any) error {
|
||||
if resp == nil || result == nil {
|
||||
return nil
|
||||
}
|
||||
body := resp.Body()
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
ct := strings.ToLower(resp.Header().Get("Content-Type"))
|
||||
ce := strings.ToLower(resp.Header().Get("Content-Encoding"))
|
||||
body := resp.Body()
|
||||
if strings.Contains(ce, "gzip") && len(body) > 0 {
|
||||
gr, gerr := gzip.NewReader(bytes.NewReader(body))
|
||||
if gerr == nil {
|
||||
defer gr.Close()
|
||||
if dec, derr := io.ReadAll(gr); derr == nil {
|
||||
body = dec
|
||||
resp.SetBody(body)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(ce, "deflate") && len(body) > 0 {
|
||||
// 处理 deflate 压缩
|
||||
dr := flate.NewReader(bytes.NewReader(body))
|
||||
defer dr.Close()
|
||||
if dec, derr := io.ReadAll(dr); derr == nil {
|
||||
body = dec
|
||||
resp.SetBody(body)
|
||||
}
|
||||
} else if strings.Contains(ce, "br") && len(body) > 0 {
|
||||
// 处理 brotli 压缩
|
||||
br := brotli.NewReader(bytes.NewReader(body))
|
||||
if dec, derr := io.ReadAll(br); derr == nil {
|
||||
body = dec
|
||||
resp.SetBody(body) // 将解压后的 body 写回 response
|
||||
}
|
||||
}
|
||||
if result != nil && (strings.Contains(ct, "application/json") || json.Valid(body)) {
|
||||
if strings.Contains(ct, "application/json") || json.Valid(body) {
|
||||
if err := json.Unmarshal(body, result); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -230,114 +417,111 @@ func (request *RestyClient) decodeResponse(resp *resty.Response, result interfac
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestyGet 发送 GET 请求
|
||||
func (request *RestyClient) RestyGet(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) {
|
||||
return r.Get(path)
|
||||
})
|
||||
if resp == nil && err != nil {
|
||||
return nil, err
|
||||
func (request *RestyClient) execute(method string, path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
client := request.newRequestClient(redirectCount)
|
||||
|
||||
doRequest := func(extraHeaders map[string]string) (*resty.Response, error) {
|
||||
r := request.buildReqRequest(client, path, extraHeaders, cookies)
|
||||
if data != nil {
|
||||
r.SetBody(data)
|
||||
}
|
||||
|
||||
var (
|
||||
resp *req.Response
|
||||
err error
|
||||
)
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
resp, err = r.Get(path)
|
||||
case http.MethodPost:
|
||||
resp, err = r.Post(path)
|
||||
case http.MethodPut:
|
||||
resp, err = r.Put(path)
|
||||
case http.MethodPatch:
|
||||
resp, err = r.Patch(path)
|
||||
case http.MethodDelete:
|
||||
resp, err = r.Delete(path)
|
||||
case http.MethodHead:
|
||||
resp, err = r.Head(path)
|
||||
case http.MethodOptions:
|
||||
resp, err = r.Options(path)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
|
||||
restyResp, adaptErr := request.adaptReqResponse(path, method, data, extraHeaders, cookies, resp)
|
||||
if err != nil && errors.Is(err, http.ErrUseLastResponse) {
|
||||
err = nil
|
||||
}
|
||||
if adaptErr != nil && err == nil {
|
||||
err = adaptErr
|
||||
}
|
||||
return restyResp, err
|
||||
}
|
||||
|
||||
if err := request.decodeResponse(resp, result); err != nil {
|
||||
return nil, err
|
||||
resp, err := doRequest(headers)
|
||||
if err == nil && resp != nil && strings.Contains(strings.ToLower(resp.Header().Get("Content-Encoding")), "zstd") {
|
||||
err = fmt.Errorf("zstd body requires identity fallback")
|
||||
}
|
||||
if err == nil {
|
||||
if decodeErr := request.decodeResponse(resp, result); decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return resp, err
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "gzip") || strings.Contains(errStr, "magic number mismatch") || strings.Contains(errStr, "zstd") || strings.Contains(errStr, "brotli") || strings.Contains(errStr, "flate") {
|
||||
h2 := map[string]string{}
|
||||
for k, v := range headers {
|
||||
if strings.ToLower(k) != "accept-encoding" {
|
||||
h2[k] = v
|
||||
}
|
||||
}
|
||||
h2["Accept-Encoding"] = "identity"
|
||||
|
||||
resp2, err2 := doRequest(h2)
|
||||
if err2 == nil {
|
||||
if decodeErr := request.decodeResponse(resp2, result); decodeErr != nil {
|
||||
return nil, decodeErr
|
||||
}
|
||||
return resp2, nil
|
||||
}
|
||||
if resp2 != nil {
|
||||
return resp2, err2
|
||||
}
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
return resp, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// RestyPost 发送 POST 请求
|
||||
func (request *RestyClient) RestyPost(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) {
|
||||
return r.SetBody(data).Post(path)
|
||||
})
|
||||
if resp == nil && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := request.decodeResponse(resp, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, err
|
||||
func (request *RestyClient) RestyGet(path string, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodGet, path, nil, result, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
// RestyPut 发送 PUT 请求
|
||||
// 功能:发送 PUT,支持请求级 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) {
|
||||
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||
return r.SetBody(data).Put(path)
|
||||
})
|
||||
if resp == nil && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := request.decodeResponse(resp, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, err
|
||||
func (request *RestyClient) RestyPost(path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodPost, path, data, result, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
// RestyPatch 发送 PATCH 请求
|
||||
// 功能:发送 PATCH,支持请求级 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) {
|
||||
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
|
||||
return r.SetBody(data).Patch(path)
|
||||
})
|
||||
if resp == nil && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := request.decodeResponse(resp, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, err
|
||||
func (request *RestyClient) RestyPut(path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodPut, path, data, result, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
// RestyDelete 发送 DELETE 请求
|
||||
// 功能:发送 DELETE,支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
|
||||
// 返回:响应对象与错误信息
|
||||
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) {
|
||||
return r.Delete(path)
|
||||
})
|
||||
if resp == nil && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := request.decodeResponse(resp, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, err
|
||||
func (request *RestyClient) RestyPatch(path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodPatch, path, data, result, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
// RestyHead 发送 HEAD 请求
|
||||
// 功能:发送 HEAD,支持请求级 headers 覆盖客户端默认;HEAD 通常无正文
|
||||
// 返回:响应对象与错误信息
|
||||
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) {
|
||||
return r.Head(path)
|
||||
})
|
||||
if resp == nil && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, err
|
||||
func (request *RestyClient) RestyDelete(path string, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodDelete, path, nil, result, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
// 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
|
||||
func (request *RestyClient) RestyHead(path string, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodHead, path, nil, nil, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
func (request *RestyClient) RestyOptions(path string, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
|
||||
return request.execute(http.MethodOptions, path, nil, nil, headers, cookies, redirectCount)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user