264 lines
6.7 KiB
Go
264 lines
6.7 KiB
Go
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"
|
|
)
|
|
|
|
var instances map[string]*Flaresolverr = make(map[string]*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 {
|
|
return instance, nil
|
|
}
|
|
flareSolverr := &Flaresolverr{
|
|
url: flaresolverrURL,
|
|
v1Url: strings.TrimSuffix(flaresolverrURL, "/") + "/v1",
|
|
sessionID: sessionID,
|
|
proxy: proxy,
|
|
}
|
|
instances[flaresolverrURL] = flareSolverr
|
|
return flareSolverr, nil
|
|
}
|
|
|
|
func (f *Flaresolverr) requestV1(req *V1RequestBase) (*V1ResponseBase, error) {
|
|
resp, err := restyClient.R().SetBody(req).Post(f.v1Url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("request failed: status code: %v", resp.StatusCode())
|
|
}
|
|
var res V1ResponseBase
|
|
err = json.Unmarshal(resp.Body(), &res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res.Solution != nil {
|
|
if res.Solution.RawCookies != nil {
|
|
for _, cookie := range res.Solution.RawCookies {
|
|
sec := int64(cookie.Expires)
|
|
nsec := int64((cookie.Expires - float64(sec)) * 1e9)
|
|
res.Solution.Cookies = append(res.Solution.Cookies, &http.Cookie{
|
|
Name: cookie.Name,
|
|
Value: cookie.Value,
|
|
Expires: time.Unix(sec, nsec),
|
|
Domain: cookie.Domain,
|
|
Path: cookie.Path,
|
|
Secure: cookie.Secure,
|
|
HttpOnly: cookie.HttpOnly,
|
|
})
|
|
}
|
|
}
|
|
|
|
if res.Solution.RawResponse != nil {
|
|
switch v := res.Solution.RawResponse.(type) {
|
|
case string:
|
|
res.Solution.Response = res.Solution.RawResponse.(string)
|
|
case map[string]any:
|
|
jsonBytes, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.Solution.Response = string(jsonBytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
func (f *Flaresolverr) GetV1(reqURL string, req *V1RequestBase) (*V1ResponseBase, error) {
|
|
if req == nil {
|
|
req = &V1RequestBase{
|
|
MaxTimeout: 60000, // 60 秒
|
|
}
|
|
}
|
|
req.Cmd = "request.get"
|
|
req.URL = reqURL
|
|
req.Session = f.sessionID
|
|
if f.proxy != "" {
|
|
req.Proxy.Url = f.proxy
|
|
}
|
|
|
|
return f.requestV1(req)
|
|
}
|
|
|
|
func (f *Flaresolverr) PostV1(reqURL string, req *V1RequestBase) (*V1ResponseBase, error) {
|
|
if req == nil {
|
|
req = &V1RequestBase{
|
|
MaxTimeout: 60000, // 60 秒
|
|
}
|
|
}
|
|
req.Cmd = "request.post"
|
|
req.URL = reqURL
|
|
req.Session = f.sessionID
|
|
if f.proxy != "" {
|
|
req.Proxy.Url = f.proxy
|
|
}
|
|
|
|
return f.requestV1(req)
|
|
}
|
|
|
|
func (f *Flaresolverr) CreateSessionV1(sessionID string) (*V1ResponseBase, error) {
|
|
req := &V1RequestBase{
|
|
Session: sessionID,
|
|
Cmd: "sessions.create",
|
|
}
|
|
if f.proxy != "" {
|
|
req.Proxy.Url = f.proxy
|
|
}
|
|
return f.requestV1(req)
|
|
}
|
|
|
|
func (f *Flaresolverr) DestroySessionV1(sessionID string) (*V1ResponseBase, error) {
|
|
req := &V1RequestBase{
|
|
Session: sessionID,
|
|
Cmd: "sessions.destroy",
|
|
}
|
|
return f.requestV1(req)
|
|
}
|
|
|
|
func (f *Flaresolverr) ListSessionsV1(sessionID string) (*V1ResponseBase, error) {
|
|
req := &V1RequestBase{
|
|
Session: sessionID,
|
|
Cmd: "sessions.list",
|
|
}
|
|
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 (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
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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) 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...)
|
|
}
|