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...) }