Files
goyomi/internal/httpclient/flaresolverr.go
T
Achmad b992080c95 feat: add FlareSolverr proxy support with DB-backed config
- Add config table for storing FlareSolverr proxy setting
- Add HTTP endpoints to get/set proxy status (GET/POST /api/config/flaresolverr)
- Refactor httpclient to support proxy mode (requests go through FlareSolverr)
- Add verbose logging for debugging
- Add POST support to FlareSolverr client

Usage:
  GET /api/config/flaresolverr - returns {flaresolverr_proxy: bool}
  POST /api/config/flaresolverr - body: {enabled: true/false}
2026-05-11 09:25:48 +00:00

120 lines
3.1 KiB
Go
Executable File

package httpclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
)
type FlareSolverrClient struct {
endpoint string
client *http.Client
}
func NewFlareSolverrClient() (*FlareSolverrClient, error) {
ep := os.Getenv("FLARESOLVERR_URL")
if ep == "" {
return nil, fmt.Errorf("FLARESOLVERR_URL not set")
}
return &FlareSolverrClient{
endpoint: ep,
client: &http.Client{},
}, nil
}
type flareSolverrRequest struct {
Cmd string `json:"cmd"`
URL string `json:"url"`
MaxTimeout int `json:"maxTimeout"`
}
type FlareSolverrResponse struct {
Status string `json:"status"`
Solution struct {
Response string `json:"response"`
Cookies []fsCookie `json:"cookies"`
Headers map[string]any `json:"headers"`
URL string `json:"url"`
Status int `json:"status"`
} `json:"solution"`
}
type fsCookie struct {
Name string `json:"name"`
Value string `json:"value"`
Domain string `json:"domain"`
Path string `json:"path"`
Expires float64 `json:"expires"`
HTTPOnly bool `json:"httpOnly"`
Secure bool `json:"secure"`
}
// Get fetches a Cloudflare-protected URL via FlareSolverr.
// Returns rendered HTML and extracted cookies.
func (f *FlareSolverrClient) Get(ctx context.Context, url string) (html string, cookies []*http.Cookie, err error) {
return f.request(ctx, "request.get", url, "", nil)
}
func (f *FlareSolverrClient) Post(ctx context.Context, url string, body string) (html string, cookies []*http.Cookie, err error) {
return f.request(ctx, "request.post", url, body, map[string]string{"Content-Type": "application/x-www-form-urlencoded"})
}
func (f *FlareSolverrClient) request(ctx context.Context, cmd, url, body string, headers map[string]string) (html string, cookies []*http.Cookie, err error) {
type fsRequest struct {
Cmd string `json:"cmd"`
URL string `json:"url"`
PostData string `json:"postData,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
MaxTimeout int `json:"maxTimeout"`
}
req := fsRequest{
Cmd: cmd,
URL: url,
MaxTimeout: 60000,
}
if body != "" {
req.PostData = body
}
if headers != nil {
req.Headers = headers
}
payload, _ := json.Marshal(req)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, f.endpoint+"/v1", bytes.NewReader(payload))
if err != nil {
return "", nil, err
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := f.client.Do(httpReq)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
var fsResp FlareSolverrResponse
if err := json.NewDecoder(resp.Body).Decode(&fsResp); err != nil {
return "", nil, err
}
if fsResp.Status != "ok" {
return "", nil, fmt.Errorf("flaresolverr: status %q", fsResp.Status)
}
for _, c := range fsResp.Solution.Cookies {
cookies = append(cookies, &http.Cookie{
Name: c.Name,
Value: c.Value,
Domain: c.Domain,
Path: c.Path,
HttpOnly: c.HTTPOnly,
Secure: c.Secure,
})
}
return fsResp.Solution.Response, cookies, nil
}