From c14db529f045d5c69403453ba7852cc151224c58 Mon Sep 17 00:00:00 2001 From: nite Date: Sat, 13 Jun 2026 15:54:21 +1000 Subject: [PATCH] feat!: migrate simulate requests to resty v3 and add instance options --- flaresolverr.go | 188 +++++++++++++++----------------------- go.mod | 22 +---- go.sum | 52 +---------- request.go | 13 ++- test/flaresolverr_test.go | 51 ++++++++--- 5 files changed, 123 insertions(+), 203 deletions(-) diff --git a/flaresolverr.go b/flaresolverr.go index 6b129b2..16aca09 100644 --- a/flaresolverr.go +++ b/flaresolverr.go @@ -3,44 +3,76 @@ package flaresolverr import ( "encoding/json" "fmt" - "io" "net/http" - "regexp" "strings" "time" - fhttp "github.com/bogdanfinn/fhttp" - tls_client "github.com/bogdanfinn/tls-client" - "github.com/bogdanfinn/tls-client/profiles" + "resty.dev/v3" ) -var instances map[string]*Flaresolverr = make(map[string]*Flaresolverr) +type instanceKey struct { + url string + sessionID string + proxy string +} + +type instanceOptions struct { + sessionID string + proxy string +} + +type InstanceOption func(*instanceOptions) + +func WithSessionID(sessionID string) InstanceOption { + return func(opts *instanceOptions) { + opts.sessionID = sessionID + } +} + +func WithProxy(proxy string) InstanceOption { + return func(opts *instanceOptions) { + opts.proxy = proxy + } +} + +var instances map[instanceKey]*Flaresolverr = make(map[instanceKey]*Flaresolverr) type Flaresolverr struct { url string v1Url string sessionID string proxy string - userAgent string - cookies []*http.Cookie } -func GetInstance(flaresolverrURL string, sessionID string, proxy string) (*Flaresolverr, error) { - if instance, ok := instances[flaresolverrURL]; ok { +func GetInstance(flaresolverrURL string, opts ...InstanceOption) (*Flaresolverr, error) { + config := instanceOptions{} + for _, opt := range opts { + if opt != nil { + opt(&config) + } + } + + key := instanceKey{ + url: flaresolverrURL, + sessionID: config.sessionID, + proxy: config.proxy, + } + + if instance, ok := instances[key]; ok { return instance, nil } flareSolverr := &Flaresolverr{ url: flaresolverrURL, v1Url: strings.TrimSuffix(flaresolverrURL, "/") + "/v1", - sessionID: sessionID, - proxy: proxy, + sessionID: config.sessionID, + proxy: config.proxy, } - instances[flaresolverrURL] = flareSolverr + instances[key] = flareSolverr return flareSolverr, nil } func (f *Flaresolverr) requestV1(req *V1RequestBase) (*V1ResponseBase, error) { - resp, err := restyClient.R().SetBody(req).Post(f.v1Url) + resp, err := NewRestyClient().R().SetBody(req).Post(f.v1Url) if err != nil { return nil, err } @@ -48,7 +80,7 @@ func (f *Flaresolverr) requestV1(req *V1RequestBase) (*V1ResponseBase, error) { return nil, fmt.Errorf("request failed: status code: %v", resp.StatusCode()) } var res V1ResponseBase - err = json.Unmarshal(resp.Body(), &res) + err = json.Unmarshal(resp.Bytes(), &res) if err != nil { return nil, err } @@ -145,106 +177,32 @@ func (f *Flaresolverr) ListSessionsV1(sessionID string) (*V1ResponseBase, error) return f.requestV1(req) } -func (f *Flaresolverr) DetermineProfile(userAgent string) profiles.ClientProfile { - re := regexp.MustCompile(`Chrome/(\d+)\.`) - matches := re.FindStringSubmatch(userAgent) - - if len(matches) < 2 { - return profiles.Chrome_120 - } - - ver := matches[1] - - switch ver { - case "124", "123", "122", "121", "120": - return profiles.Chrome_120 - case "119", "118", "117": - return profiles.Chrome_117 - default: - return profiles.Chrome_120 +func ConvertCookiesToStr(cookies []*http.Cookie) string { + c := []string{} + for _, cookie := range cookies { + c = append(c, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)) } + return strings.Join(c, "; ") } -func (f *Flaresolverr) simulateRequest(method string, url string, ua string, cookies []*http.Cookie, body io.Reader, opts ...tls_client.HttpClientOption) (*fhttp.Response, error) { - currentUA := ua - currentCookies := cookies - - // Use cached credentials if arguments are empty - if currentUA == "" { - currentUA = f.userAgent +func (f *Flaresolverr) simulateRequest(method string, url string, ua string, cookies []*http.Cookie, body any) (*resty.Response, error) { + var resp *resty.Response + var err error + c := NewRestyClient() + if f.proxy != "" { + c = c.SetProxy(f.proxy) + } else { + c.RemoveProxy() } - if len(currentCookies) == 0 { - currentCookies = f.cookies - } - - // Helper to perform the request - doRequest := func(ua string, cookies []*http.Cookie) (*fhttp.Response, error) { - profile := f.DetermineProfile(ua) - reqOpts := append([]tls_client.HttpClientOption{}, opts...) // Clone opts - reqOpts = append(reqOpts, tls_client.WithClientProfile(profile)) - if f.proxy != "" { - reqOpts = append(reqOpts, tls_client.WithProxyUrl(f.proxy)) - } - - client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), reqOpts...) - if err != nil { - return nil, fmt.Errorf("failed to create client: %w", err) - } - req, err := fhttp.NewRequest(method, url, body) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("User-Agent", ua) - for _, cookie := range cookies { - req.AddCookie(&fhttp.Cookie{ - Name: cookie.Name, - Value: cookie.Value, - Path: cookie.Path, - Domain: cookie.Domain, - Expires: cookie.Expires, - RawExpires: cookie.RawExpires, - MaxAge: cookie.MaxAge, - Secure: cookie.Secure, - HttpOnly: cookie.HttpOnly, - SameSite: fhttp.SameSite(cookie.SameSite), - Raw: cookie.Raw, - Unparsed: cookie.Unparsed, - }) - } - return client.Do(req) - } - - // First attempt - resp, err := doRequest(currentUA, currentCookies) - - // Check for retry condition (err or 403) - needRetry := false - if err == nil && (resp.StatusCode == 403 || resp.StatusCode == 503) { - needRetry = true - } - - if needRetry || (currentUA == "" && len(currentCookies) == 0) { - // Refresh credentials - fmt.Println("[Flaresolverr] Refreshing credentials via GetV1...") - v1Resp, err := f.GetV1(url, nil) - if err != nil { - return nil, fmt.Errorf("failed to refresh credentials: %w", err) - } - if v1Resp.Solution.Status != 200 { - return nil, fmt.Errorf("refresh failed with status: %d", v1Resp.Solution.Status) - } - - // Update state - currentUA = v1Resp.Solution.UserAgent - currentCookies = v1Resp.Solution.Cookies - f.userAgent = currentUA - f.cookies = currentCookies - - // Retry request - resp, err = doRequest(currentUA, currentCookies) - if err != nil { - return nil, fmt.Errorf("retry failed: %w", err) - } + req := c.R(). + SetHeader("User-Agent", ua). + SetHeader("Cookie", ConvertCookiesToStr(cookies)). + SetHeader("Accept-Encoding", "gzip, deflate") + switch strings.ToUpper(method) { + case "GET": + resp, err = req.Get(url) + case "POST": + resp, err = req.SetBody(body).Post(url) } if err != nil { @@ -254,10 +212,10 @@ func (f *Flaresolverr) simulateRequest(method string, url string, ua string, coo return resp, nil } -func (f *Flaresolverr) SimulateGet(url string, ua string, cookies []*http.Cookie, opts ...tls_client.HttpClientOption) (*fhttp.Response, error) { - return f.simulateRequest("GET", url, ua, cookies, nil, opts...) +func (f *Flaresolverr) SimulateGet(url string, ua string, cookies []*http.Cookie) (*resty.Response, error) { + return f.simulateRequest("GET", url, ua, cookies, nil) } -func (f *Flaresolverr) SimulatePost(url string, ua string, cookies []*http.Cookie, body io.Reader, opts ...tls_client.HttpClientOption) (*fhttp.Response, error) { - return f.simulateRequest("POST", url, ua, cookies, body, opts...) +func (f *Flaresolverr) SimulatePost(url string, ua string, cookies []*http.Cookie, body any) (*resty.Response, error) { + return f.simulateRequest("POST", url, ua, cookies, body) } diff --git a/go.mod b/go.mod index a567ce6..808d873 100644 --- a/go.mod +++ b/go.mod @@ -2,24 +2,6 @@ module git.nite07.com/nite/go-flaresolverr go 1.24.1 -require ( - github.com/bogdanfinn/fhttp v0.6.7 - github.com/bogdanfinn/tls-client v1.13.2-0.20260121072726-145b2fdcf1c7 - github.com/go-resty/resty/v2 v2.17.1 -) +require resty.dev/v3 v3.0.0-rc.1 -require ( - github.com/andybalholm/brotli v1.2.0 // indirect - github.com/bdandy/go-errors v1.2.2 // indirect - github.com/bdandy/go-socks4 v1.2.3 // indirect - github.com/bogdanfinn/quic-go-utls v1.0.7-utls // indirect - github.com/bogdanfinn/utls v1.7.7-barnius // indirect - github.com/bogdanfinn/websocket v1.5.4-barnius // indirect - github.com/klauspost/compress v1.18.2 // indirect - github.com/quic-go/qpack v0.6.0 // indirect - github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect -) +require golang.org/x/net v0.49.0 // indirect diff --git a/go.sum b/go.sum index 987e5a1..6cbea5d 100644 --- a/go.sum +++ b/go.sum @@ -1,52 +1,4 @@ -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= -github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= -github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= -github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI= -github.com/bogdanfinn/fhttp v0.6.7 h1:yTDywa9INbRqePBE5gHhpxlMjvAQ0bdX77pvOTPJoPI= -github.com/bogdanfinn/fhttp v0.6.7/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M= -github.com/bogdanfinn/quic-go-utls v1.0.7-utls h1:opxU/wt2C6FcD3rkGSOwfpQgfGSFx9eAKYQrFwYBzuo= -github.com/bogdanfinn/quic-go-utls v1.0.7-utls/go.mod h1:bk8QMY2KypO8A6LzHJ7C4+bdB0ksLOd6NZt600wXYe8= -github.com/bogdanfinn/tls-client v1.13.2-0.20260121072726-145b2fdcf1c7 h1:7/7xbSyvJTwxjOP5iBw5CM3SmIBGqHwC5nFkbkvdI28= -github.com/bogdanfinn/tls-client v1.13.2-0.20260121072726-145b2fdcf1c7/go.mod h1:z01rwJcKsjx8Vo0dXKwz7YRkqo+XNIKC0pJaCdJDHgs= -github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU= -github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg= -github.com/bogdanfinn/websocket v1.5.4-barnius h1:qn3DU/KMHMNPNnwDCtA/IN3QqmzV98DVsvQkQkxruKw= -github.com/bogdanfinn/websocket v1.5.4-barnius/go.mod h1:IWHoWLZd+5/o9340A/m/ApAOBmWDSTUXuL7zSVmRHQk= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= -github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +resty.dev/v3 v3.0.0-rc.1 h1:oK7Y6+r0JSSrj9Szxs4yv8Wd/pW/HZhHiPy1QmhND5M= +resty.dev/v3 v3.0.0-rc.1/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= diff --git a/request.go b/request.go index 77ebb3b..84db658 100644 --- a/request.go +++ b/request.go @@ -4,19 +4,18 @@ import ( "net/http" "time" - "github.com/go-resty/resty/v2" + "resty.dev/v3" ) -var restyClient *resty.Client - -func init() { - restyClient = resty.New() - restyClient.SetRetryCount(3).SetRetryWaitTime(3 * time.Second).AddRetryCondition( +func NewRestyClient() *resty.Client { + restyClient := resty.New() + restyClient.SetRetryCount(3).SetRetryWaitTime(3 * time.Second).AddRetryConditions( func(r *resty.Response, err error) bool { return err != nil || r.StatusCode() == http.StatusTooManyRequests }, ) - restyClient.SetLogger(disableLogger{}).SetHeader("Accept-Charset", "utf-8").SetHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0") + restyClient.SetLogger(disableLogger{}).SetHeader("Accept-Charset", "utf-8") + return restyClient } type disableLogger struct{} diff --git a/test/flaresolverr_test.go b/test/flaresolverr_test.go index 2130a30..50ba627 100644 --- a/test/flaresolverr_test.go +++ b/test/flaresolverr_test.go @@ -1,19 +1,21 @@ package test import ( - "io" "testing" "git.nite07.com/nite/go-flaresolverr" ) func TestGetV1(t *testing.T) { - f, err := flaresolverr.GetInstance("http://10.10.10.1:8191", "", "socks5://10.10.10.1:7900") + f, err := flaresolverr.GetInstance( + "http://10.10.10.1:8191", + flaresolverr.WithProxy("socks5://10.10.10.1:7900"), + ) if err != nil { t.Error(err) return } - resp, err := f.GetV1("https://x1337x.cc/", nil) + resp, err := f.GetV1("https://steamrip.com/games-list-page/", nil) if err != nil { t.Error(err) return @@ -22,19 +24,46 @@ func TestGetV1(t *testing.T) { t.Error("status code != 200", resp.Solution.Status) return } - resp2, err := f.SimulateGet("https://x1337x.cc/user/FitGirl/", resp.Solution.UserAgent, resp.Solution.Cookies) + resp2, err := f.SimulateGet("https://steamrip.com/games-list-page/", resp.Solution.UserAgent, resp.Solution.Cookies) if err != nil { t.Error(err) return } - if resp2.StatusCode != 200 { - t.Error("status code != 200", resp2.StatusCode) - return - } - body, err := io.ReadAll(resp2.Body) - if err != nil { - t.Error(err) + if resp2.StatusCode() != 200 { + t.Error("status code != 200", resp2.StatusCode()) return } + body := resp2.Bytes() + t.Log(string(body)) +} + +func TestGetV1_2(t *testing.T) { + f, err := flaresolverr.GetInstance( + "http://10.10.10.1:8191", + flaresolverr.WithProxy("socks5://10.10.10.1:7900"), + ) + if err != nil { + t.Error(err) + return + } + resp, err := f.GetV1("https://1337x.to", nil) + if err != nil { + t.Error(err) + return + } + if resp.Solution.Status != 200 { + t.Error("status code != 200", resp.Solution.Status) + return + } + resp2, err := f.SimulateGet("https://1337x.to/user/FitGirl/", resp.Solution.UserAgent, resp.Solution.Cookies) + if err != nil { + t.Error(err) + return + } + if resp2.StatusCode() != 200 { + t.Error("status code != 200", resp2.StatusCode()) + return + } + body := resp2.Bytes() t.Log(string(body)) }