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}
This commit is contained in:
@@ -2,7 +2,7 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -o ./tmp/goyomi ./cmd/server"
|
cmd = "go build -buildvcs=false -o ./tmp/goyomi ./cmd/server"
|
||||||
bin = "./tmp/goyomi"
|
bin = "./tmp/goyomi"
|
||||||
include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml", "json"]
|
include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml", "json"]
|
||||||
exclude_dir = ["tmp", "vendor", ".git"]
|
exclude_dir = ["tmp", "vendor", ".git"]
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
# Go tooling
|
# Go tooling
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
*.log
|
||||||
/vendor/
|
/vendor/
|
||||||
|
tmp/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
DEV_COMPOSE = docker compose -f compose-dev.yml --env-file dev.env
|
DEV_COMPOSE = docker compose -f compose-dev.yml
|
||||||
|
DEV_GO = /usr/local/go/bin/go
|
||||||
|
|
||||||
.PHONY: dev-up dev-shell dev-down dev-logs dev-build dev-run smoke-keyoapp-plain smoke-keyoapp-plain-dev
|
.PHONY: dev-up dev-shell dev-down dev-logs dev-build dev-run smoke-keyoapp-plain smoke-keyoapp-plain-dev
|
||||||
|
|
||||||
@@ -25,4 +26,4 @@ smoke-keyoapp-plain:
|
|||||||
|
|
||||||
smoke-keyoapp-plain-dev:
|
smoke-keyoapp-plain-dev:
|
||||||
$(DEV_COMPOSE) up -d postgres flaresolverr dev
|
$(DEV_COMPOSE) up -d postgres flaresolverr dev
|
||||||
$(DEV_COMPOSE) exec dev sh -lc "GOYOMI_SMOKE=1 go test -count=1 -v ./sources -run TestStandaloneKeyoappSmoke"
|
$(DEV_COMPOSE) exec dev sh -lc "GOYOMI_SMOKE=1 $(DEV_GO) test -count=1 -v ./sources -run TestStandaloneKeyoappSmoke"
|
||||||
@@ -2,12 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"goyomi/internal/config"
|
"goyomi/internal/config"
|
||||||
"goyomi/internal/db"
|
"goyomi/internal/db"
|
||||||
|
"goyomi/internal/httpclient"
|
||||||
_ "goyomi/internal/registry"
|
_ "goyomi/internal/registry"
|
||||||
_ "goyomi/sources/all/hentaihand"
|
_ "goyomi/sources/all/hentaihand"
|
||||||
_ "goyomi/sources/all/kemono"
|
_ "goyomi/sources/all/kemono"
|
||||||
@@ -40,13 +43,56 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
|
// Initialize config manager
|
||||||
|
config.InitConfigManager(database.Queries)
|
||||||
|
|
||||||
|
if os.Getenv("FLARESOLVERR_URL") != "" {
|
||||||
|
fsClient, err := httpclient.NewFlareSolverrClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("flaresolverr: client creation failed: %v", err)
|
||||||
|
} else {
|
||||||
|
httpclient.SetGlobalFlareSolverr(fsClient)
|
||||||
|
log.Printf("flaresolverr: client initialized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintln(w, "ok")
|
fmt.Fprintln(w, "ok")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Config endpoints
|
||||||
|
mux.HandleFunc("/api/config/flaresolverr", handleFlareSolverrConfig)
|
||||||
|
|
||||||
log.Printf("listening on %s", cfg.Addr)
|
log.Printf("listening on %s", cfg.Addr)
|
||||||
if err := http.ListenAndServe(cfg.Addr, mux); err != nil {
|
if err := http.ListenAndServe(cfg.Addr, mux); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleFlareSolverrConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
enabled := config.IsFlareSolverrProxyEnabled()
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{"flaresolverr_proxy": enabled})
|
||||||
|
|
||||||
|
case "POST":
|
||||||
|
var req struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := config.SetFlareSolverrProxyEnabled(ctx, req.Enabled); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{"flaresolverr_proxy": req.Enabled})
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,3 +19,4 @@ CHAPTER_LIST_TTL_SECONDS=600
|
|||||||
|
|
||||||
# FlareSolverr
|
# FlareSolverr
|
||||||
FLARESOLVERR_LOG_LEVEL=info
|
FLARESOLVERR_LOG_LEVEL=info
|
||||||
|
FLARESOLVERR_PROXY=0
|
||||||
|
|||||||
+24
-15
@@ -7,26 +7,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DatabaseURL string
|
DatabaseURL string
|
||||||
FlareSolverrURL string
|
FlareSolverrURL string
|
||||||
Addr string
|
FlareSolverrProxy bool
|
||||||
MangaListTTL time.Duration
|
Addr string
|
||||||
MangaDetailTTL time.Duration
|
MangaListTTL time.Duration
|
||||||
|
MangaDetailTTL time.Duration
|
||||||
ChapterListTTL time.Duration
|
ChapterListTTL time.Duration
|
||||||
DBMaxConns int
|
DBMaxConns int
|
||||||
DBMinConns int
|
DBMinConns int
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
return Config{
|
return Config{
|
||||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||||
FlareSolverrURL: os.Getenv("FLARESOLVERR_URL"),
|
FlareSolverrURL: os.Getenv("FLARESOLVERR_URL"),
|
||||||
Addr: envStr("ADDR", ":8080"),
|
FlareSolverrProxy: envBool("FLARESOLVERR_PROXY", false),
|
||||||
MangaListTTL: time.Duration(envInt("MANGA_LIST_TTL_SECONDS", 600)) * time.Second,
|
Addr: envStr("ADDR", ":8080"),
|
||||||
MangaDetailTTL: time.Duration(envInt("MANGA_DETAIL_TTL_SECONDS", 3600)) * time.Second,
|
MangaListTTL: time.Duration(envInt("MANGA_LIST_TTL_SECONDS", 600)) * time.Second,
|
||||||
ChapterListTTL: time.Duration(envInt("CHAPTER_LIST_TTL_SECONDS", 600)) * time.Second,
|
MangaDetailTTL: time.Duration(envInt("MANGA_DETAIL_TTL_SECONDS", 3600)) * time.Second,
|
||||||
DBMaxConns: envInt("DB_MAX_CONNS", 10),
|
ChapterListTTL: time.Duration(envInt("CHAPTER_LIST_TTL_SECONDS", 600)) * time.Second,
|
||||||
DBMinConns: envInt("DB_MIN_CONNS", 2),
|
DBMaxConns: envInt("DB_MAX_CONNS", 10),
|
||||||
|
DBMinConns: envInt("DB_MIN_CONNS", 2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,3 +47,10 @@ func envInt(key string, def int) int {
|
|||||||
}
|
}
|
||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envBool(key string, def bool) bool {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v == "1" || v == "true" || v == "yes"
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"goyomi/internal/db/queries"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
flaresolverrProxy bool
|
||||||
|
queries *queries.Queries
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
manager *ConfigManager
|
||||||
|
managerOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitConfigManager(q *queries.Queries) {
|
||||||
|
managerOnce.Do(func() {
|
||||||
|
manager = &ConfigManager{queries: q}
|
||||||
|
manager.loadFromDB(context.Background())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ConfigManager) loadFromDB(ctx context.Context) {
|
||||||
|
if m.queries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val, err := m.queries.GetConfigValue(ctx, "flaresolverr_proxy")
|
||||||
|
if err == nil && val != "" {
|
||||||
|
m.flaresolverrProxy = val == "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFlareSolverrProxyEnabled() bool {
|
||||||
|
if manager == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
manager.mu.RLock()
|
||||||
|
defer manager.mu.RUnlock()
|
||||||
|
return manager.flaresolverrProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFlareSolverrProxyEnabled(ctx context.Context, enabled bool) error {
|
||||||
|
if manager == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := "false"
|
||||||
|
if enabled {
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
err := manager.queries.SetConfigValue(ctx, queries.SetConfigValueParams{
|
||||||
|
Key: "flaresolverr_proxy",
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manager.mu.Lock()
|
||||||
|
manager.flaresolverrProxy = enabled
|
||||||
|
manager.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS config;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
key VARCHAR(256) PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO config (key, value) VALUES ('flaresolverr_proxy', 'false') ON CONFLICT DO NOTHING;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- name: GetConfigValue :one
|
||||||
|
SELECT value FROM config WHERE key = $1;
|
||||||
|
|
||||||
|
-- name: SetConfigValue :exec
|
||||||
|
INSERT INTO config (key, value) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.31.1
|
||||||
|
// source: config.sql
|
||||||
|
|
||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getConfigValue = `-- name: GetConfigValue :one
|
||||||
|
SELECT value FROM config WHERE key = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetConfigValue(ctx context.Context, key string) (string, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getConfigValue, key)
|
||||||
|
var value string
|
||||||
|
err := row.Scan(&value)
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const setConfigValue = `-- name: SetConfigValue :exec
|
||||||
|
INSERT INTO config (key, value) VALUES ($1, $2)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetConfigValueParams struct {
|
||||||
|
Key string `db:"key" json:"key"`
|
||||||
|
Value string `db:"value" json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetConfigValue(ctx context.Context, arg SetConfigValueParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, setConfigValue, arg.Key, arg.Value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -23,6 +23,11 @@ type Chapter struct {
|
|||||||
PageCount int32 `db:"page_count" json:"page_count"`
|
PageCount int32 `db:"page_count" json:"page_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Key string `db:"key" json:"key"`
|
||||||
|
Value string `db:"value" json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
type Manga struct {
|
type Manga struct {
|
||||||
ID int32 `db:"id" json:"id"`
|
ID int32 `db:"id" json:"id"`
|
||||||
SourceID int64 `db:"source_id" json:"source_id"`
|
SourceID int64 `db:"source_id" json:"source_id"`
|
||||||
|
|||||||
+134
-11
@@ -2,23 +2,58 @@ package httpclient
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"goyomi/internal/config"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultUserAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
|
const defaultUserAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalFSClient *FlareSolverrClient
|
||||||
|
globalProxy bool
|
||||||
|
verboseLog bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetGlobalFlareSolverr(client *FlareSolverrClient) {
|
||||||
|
globalFSClient = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetGlobalProxy(enabled bool) {
|
||||||
|
globalProxy = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetVerboseLog(enabled bool) {
|
||||||
|
verboseLog = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProxyEnabled() bool {
|
||||||
|
if globalProxy {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if config.IsFlareSolverrProxyEnabled() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
http *http.Client
|
http *http.Client
|
||||||
rateLimit float64
|
fsClient *FlareSolverrClient
|
||||||
burst int
|
proxy bool
|
||||||
referer string
|
rateLimit float64
|
||||||
|
burst int
|
||||||
|
referer string
|
||||||
|
verboseLog bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
limiters map[string]*rate.Limiter
|
limiters map[string]*rate.Limiter
|
||||||
@@ -41,16 +76,28 @@ func WithReferer(referer string) Option {
|
|||||||
return func(c *Client) { c.referer = referer }
|
return func(c *Client) { c.referer = referer }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithFlareSolverr(fs *FlareSolverrClient) Option {
|
||||||
|
return func(c *Client) { c.fsClient = fs }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithProxy(enabled bool) Option {
|
||||||
|
return func(c *Client) { c.proxy = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithVerboseLog(enabled bool) Option {
|
||||||
|
return func(c *Client) { c.verboseLog = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
func NewClient(opts ...Option) *Client {
|
func NewClient(opts ...Option) *Client {
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
c := &Client{
|
c := &Client{
|
||||||
http: &http.Client{
|
http: &http.Client{Timeout: 30 * time.Second, Jar: jar},
|
||||||
Timeout: 30 * time.Second,
|
fsClient: globalFSClient,
|
||||||
Jar: jar,
|
proxy: globalProxy,
|
||||||
},
|
rateLimit: 1,
|
||||||
rateLimit: 1,
|
burst: 1,
|
||||||
burst: 1,
|
limiters: map[string]*rate.Limiter{},
|
||||||
limiters: map[string]*rate.Limiter{},
|
verboseLog: verboseLog,
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(c)
|
o(c)
|
||||||
@@ -70,6 +117,13 @@ func (c *Client) limiter(host string) *rate.Limiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
if (c.proxy || ProxyEnabled()) && c.fsClient != nil {
|
||||||
|
return c.doViaFlareSolverr(req)
|
||||||
|
}
|
||||||
|
return c.doDirect(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doDirect(req *http.Request) (*http.Response, error) {
|
||||||
if err := c.limiter(req.URL.Host).Wait(req.Context()); err != nil {
|
if err := c.limiter(req.URL.Host).Wait(req.Context()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -80,12 +134,19 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
req.Header.Set("User-Agent", defaultUserAgent)
|
req.Header.Set("User-Agent", defaultUserAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.verboseLog {
|
||||||
|
log.Printf("[httpclient] DIRECT GET %s", req.URL.String())
|
||||||
|
}
|
||||||
|
|
||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if c.verboseLog {
|
||||||
|
log.Printf("[httpclient] DIRECT RESPONSE %s status=%d", req.URL.String(), resp.StatusCode)
|
||||||
|
}
|
||||||
if resp.StatusCode != http.StatusTooManyRequests {
|
if resp.StatusCode != http.StatusTooManyRequests {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -103,6 +164,68 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) doViaFlareSolverr(req *http.Request) (*http.Response, error) {
|
||||||
|
if err := c.limiter(req.URL.Host).Wait(req.Context()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
url := req.URL.String()
|
||||||
|
|
||||||
|
if c.verboseLog {
|
||||||
|
log.Printf("[httpclient] FLARESOLVERR %s %s", req.Method, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
var html string
|
||||||
|
var cookies []*http.Cookie
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if req.Method == http.MethodGet {
|
||||||
|
html, cookies, err = c.fsClient.Get(req.Context(), url)
|
||||||
|
} else if req.Method == http.MethodPost {
|
||||||
|
var bodyStr string
|
||||||
|
if req.Body != nil {
|
||||||
|
bodyBytes, _ := io.ReadAll(req.Body)
|
||||||
|
bodyStr = string(bodyBytes)
|
||||||
|
if c.verboseLog {
|
||||||
|
log.Printf("[httpclient] FLARESOLVERR POST BODY: %s", bodyStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html, cookies, err = c.fsClient.Post(req.Context(), url, bodyStr)
|
||||||
|
} else {
|
||||||
|
return c.doDirect(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("flaresolverr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.verboseLog {
|
||||||
|
log.Printf("[httpclient] FLARESOLVERR RESPONSE length=%d cookies=%d", len(html), len(cookies))
|
||||||
|
if len(html) > 0 {
|
||||||
|
truncate := html
|
||||||
|
if len(html) > 500 {
|
||||||
|
truncate = html[:500]
|
||||||
|
}
|
||||||
|
log.Printf("[httpclient] FLARESOLVERR RESPONSE HTML (first 500 chars): %s", truncate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Header: http.Header{},
|
||||||
|
Body: io.NopCloser(strings.NewReader(html)),
|
||||||
|
}
|
||||||
|
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
|
if c.http.Jar != nil {
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
c.http.Jar.SetCookies(req.URL, []*http.Cookie{cookie})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func retryAfter(resp *http.Response) time.Duration {
|
func retryAfter(resp *http.Response) time.Duration {
|
||||||
ra := resp.Header.Get("Retry-After")
|
ra := resp.Header.Get("Retry-After")
|
||||||
if ra == "" {
|
if ra == "" {
|
||||||
|
|||||||
@@ -55,19 +55,43 @@ type fsCookie struct {
|
|||||||
// Get fetches a Cloudflare-protected URL via FlareSolverr.
|
// Get fetches a Cloudflare-protected URL via FlareSolverr.
|
||||||
// Returns rendered HTML and extracted cookies.
|
// Returns rendered HTML and extracted cookies.
|
||||||
func (f *FlareSolverrClient) Get(ctx context.Context, url string) (html string, cookies []*http.Cookie, err error) {
|
func (f *FlareSolverrClient) Get(ctx context.Context, url string) (html string, cookies []*http.Cookie, err error) {
|
||||||
payload, _ := json.Marshal(flareSolverrRequest{
|
return f.request(ctx, "request.get", url, "", nil)
|
||||||
Cmd: "request.get",
|
}
|
||||||
|
|
||||||
|
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,
|
URL: url,
|
||||||
MaxTimeout: 60000,
|
MaxTimeout: 60000,
|
||||||
})
|
}
|
||||||
|
if body != "" {
|
||||||
|
req.PostData = body
|
||||||
|
}
|
||||||
|
if headers != nil {
|
||||||
|
req.Headers = headers
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, f.endpoint+"/v1", bytes.NewReader(payload))
|
payload, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, f.endpoint+"/v1", bytes.NewReader(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := f.client.Do(req)
|
resp, err := f.client.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
exit status 1
|
|
||||||
Reference in New Issue
Block a user