2026-04-09 01:00:31 +08:00
package request
import (
"bytes"
"compress/flate"
"compress/gzip"
2026-04-17 03:12:28 +08:00
"context"
2026-04-09 01:00:31 +08:00
"encoding/json"
2026-04-17 03:12:28 +08:00
"errors"
"fmt"
2026-04-09 01:00:31 +08:00
"io"
"net/http"
"net/http/cookiejar"
2026-04-17 03:12:28 +08:00
"net/url"
2026-04-09 01:00:31 +08:00
"strings"
"time"
"github.com/andybalholm/brotli"
"github.com/go-resty/resty/v2"
2026-04-17 03:12:28 +08:00
req "github.com/imroc/req/v3"
2026-04-09 01:00:31 +08:00
"github.com/skycheung803/go-bypasser"
)
type RestyClient struct {
2026-04-17 03:12:28 +08:00
client * resty . Client
reqClient * req . Client
ctx context . Context
baseURL string
defaultHeaders map [ string ] string
proxyStr string
timeout time . Duration
2026-04-09 01:00:31 +08:00
}
func ( request * RestyClient ) Resty ( ) * resty . Client {
return request . client
}
2026-04-17 03:12:28 +08:00
// NewClient 创建一个基于 go-bypasser(req/v3) 的客户端。
// 对外继续保留 Resty 风格接口,但底层请求不再走 resty.Transport。
2026-04-09 01:00:31 +08:00
func NewClient ( baseURL string , proxyStr string , persistCookies bool , timeout int ) * RestyClient {
2026-04-17 03:12:28 +08:00
if timeout <= 0 {
timeout = 60
}
timeoutDuration := time . Duration ( timeout ) * time . Second
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" ,
}
stateClient := resty . New ( ) .
SetTimeout ( timeoutDuration ) .
SetHeaders ( defaultHeaders )
2026-04-09 01:00:31 +08:00
if baseURL != "" {
2026-04-17 03:12:28 +08:00
stateClient . SetBaseURL ( baseURL )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
var sharedJar http . CookieJar
2026-04-09 01:00:31 +08:00
if persistCookies {
jar , _ := cookiejar . New ( nil )
2026-04-17 03:12:28 +08:00
sharedJar = jar
stateClient . SetCookieJar ( sharedJar )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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 ( )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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 {
2026-04-09 01:00:31 +08:00
opts := [ ] bypasser . BypasserOption {
bypasser . WithInsecureSkipVerify ( true ) ,
}
if proxyStr != "" {
opts = append ( opts , bypasser . WithProxy ( proxyStr ) )
}
bypass , err := bypasser . NewBypasser ( opts ... )
if err != nil {
panic ( err )
}
2026-04-17 03:12:28 +08:00
rt , ok := bypass . Transport . ( * bypasser . StandardRoundTripper )
if ! ok || rt . Client == nil {
panic ( "go-bypasser did not return a StandardRoundTripper client" )
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
func ( request * RestyClient ) newRequestClient ( redirectCount int ) * req . Client {
client := request . reqClient . Clone ( )
if request . baseURL != "" {
client . SetBaseURL ( request . baseURL )
}
client . SetTimeout ( request . timeout )
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
switch {
case redirectCount == 0 :
client . SetRedirectPolicy ( req . NoRedirectPolicy ( ) )
case redirectCount > 0 :
client . SetRedirectPolicy ( req . MaxRedirectPolicy ( redirectCount ) )
default :
client . SetRedirectPolicy ( req . DefaultRedirectPolicy ( ) )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return client
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
func ( request * RestyClient ) resolveRequestURL ( path string ) * url . URL {
if path == "" {
return nil
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
parsedURL , err := url . Parse ( path )
2026-04-09 01:00:31 +08:00
if err != nil {
2026-04-17 03:12:28 +08:00
return nil
}
if parsedURL . IsAbs ( ) {
return parsedURL
}
if request . baseURL == "" {
return parsedURL
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
baseURL , err := url . Parse ( request . baseURL )
if err != nil {
return parsedURL
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return baseURL . ResolveReference ( parsedURL )
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
func cloneCookie ( cookie * http . Cookie ) * http . Cookie {
if cookie == nil {
return nil
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
copied := * cookie
return & copied
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
func isSafeHTTPCookieValue ( value string ) bool {
if value == "" {
return true
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
for _ , r := range value {
if r < 0x21 || r > 0x7e {
return false
}
switch r {
case '"' , ';' , '\\' , ',' :
return false
}
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return true
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
func parseRawCookieHeader ( raw string ) [ ] * http . Cookie {
if raw == "" {
return nil
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
func buildCookieHeader ( cookies [ ] * http . Cookie ) string {
if len ( cookies ) == 0 {
return ""
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
parts := make ( [ ] string , 0 , len ( cookies ) )
for _ , cookie := range cookies {
if cookie == nil || cookie . Name == "" {
continue
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
parts = append ( parts , fmt . Sprintf ( "%s=%s" , cookie . Name , cookie . Value ) )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return strings . Join ( parts , "; " )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
// 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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
}
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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
if _ , exists := cookieMap [ cookie . Name ] ; ! exists {
order = append ( order , cookie . Name )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
cloned := cloneCookie ( cookie )
cookieMap [ cookie . Name ] = cloned
if cloned != nil && ! isSafeHTTPCookieValue ( cloned . Value ) {
rawCookieNames [ cloned . Name ] = struct { } { }
2026-04-09 01:00:31 +08:00
}
}
2026-04-17 03:12:28 +08:00
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 )
2026-04-09 01:00:31 +08:00
}
}
2026-04-17 03:12:28 +08:00
if request . client != nil {
for _ , cookie := range request . client . Cookies {
appendCookie ( cookie )
}
}
for _ , cookie := range requestCookies {
appendCookie ( cookie )
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
mergedCookies := make ( [ ] * http . Cookie , 0 , len ( order ) )
for _ , name := range order {
if cookie := cookieMap [ name ] ; cookie != nil {
mergedCookies = append ( mergedCookies , cloneCookie ( cookie ) )
}
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
if len ( rawCookieNames ) == 0 {
return mergedCookies , ""
}
return mergedCookies , buildCookieHeader ( mergedCookies )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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 {
r . SetHeaders ( headers )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
mergedCookies , rawCookieHeader := request . prepareCookies ( path , cookies )
if rawCookieHeader != "" {
r . SetHeader ( "Cookie" , rawCookieHeader )
} else if len ( mergedCookies ) > 0 {
r . SetCookies ( mergedCookies ... )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return r
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
body , err := resp . ToBytes ( )
if err != nil && resp . Response == nil {
2026-04-09 01:00:31 +08:00
return nil , err
}
2026-04-17 03:12:28 +08:00
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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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" ) )
if strings . Contains ( ct , "application/json" ) || json . Valid ( body ) {
if err := json . Unmarshal ( body , result ) ; err != nil {
return err
}
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return nil
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
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
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
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
}
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
if resp != nil {
return resp , err
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
return nil , err
}
2026-04-09 01:00:31 +08:00
2026-04-17 03:12:28 +08:00
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 )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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 )
2026-04-09 01:00:31 +08:00
}
2026-04-17 03:12:28 +08:00
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 )
}
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 )
}
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 )
}
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 )
2026-04-09 01:00:31 +08:00
}