Files
seanime-docker/seanime-2.9.10/internal/util/round_tripper.go
2025-09-20 14:08:38 +01:00

127 lines
3.3 KiB
Go

package util
import (
"crypto/tls"
"errors"
"net/http"
"time"
)
// Full credit to https://github.com/DaRealFreak/cloudflare-bp-go
// RetryConfig configures the retry behavior
type RetryConfig struct {
MaxRetries int
RetryDelay time.Duration
TimeoutOnly bool // Only retry on timeout errors
}
// cloudFlareRoundTripper is a custom round tripper add the validated request headers.
type cloudFlareRoundTripper struct {
inner http.RoundTripper
options Options
retry *RetryConfig
}
// Options the option to set custom headers
type Options struct {
AddMissingHeaders bool
Headers map[string]string
}
// AddCloudFlareByPass returns a round tripper adding the required headers for the CloudFlare checks
// and updates the TLS configuration of the passed inner transport.
func AddCloudFlareByPass(inner http.RoundTripper, options ...Options) http.RoundTripper {
if trans, ok := inner.(*http.Transport); ok {
trans.TLSClientConfig = getCloudFlareTLSConfiguration()
}
roundTripper := &cloudFlareRoundTripper{
inner: inner,
retry: &RetryConfig{
MaxRetries: 3,
RetryDelay: 2 * time.Second,
TimeoutOnly: true,
},
}
if options != nil && len(options) > 0 {
roundTripper.options = options[0]
} else {
roundTripper.options = GetDefaultOptions()
}
return roundTripper
}
// RoundTrip adds the required request headers to pass CloudFlare checks.
func (ug *cloudFlareRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
var lastErr error
attempts := 0
for attempts <= ug.retry.MaxRetries {
// Add headers for this attempt
if ug.options.AddMissingHeaders {
for header, value := range ug.options.Headers {
if _, ok := r.Header[header]; !ok {
if header == "User-Agent" {
// Generate new random user agent for each attempt
r.Header.Set(header, GetRandomUserAgent())
} else {
r.Header.Set(header, value)
}
}
}
}
// Make the request
var resp *http.Response
var err error
// in case we don't have an inner transport layer from the round tripper
if ug.inner == nil {
resp, err = (&http.Transport{
TLSClientConfig: getCloudFlareTLSConfiguration(),
ForceAttemptHTTP2: false,
}).RoundTrip(r)
} else {
resp, err = ug.inner.RoundTrip(r)
}
// If successful or not a timeout error, return immediately
if err == nil || (ug.retry.TimeoutOnly && !errors.Is(err, http.ErrHandlerTimeout)) {
return resp, err
}
lastErr = err
attempts++
// If we have more retries, wait before next attempt
if attempts <= ug.retry.MaxRetries {
time.Sleep(ug.retry.RetryDelay)
}
}
return nil, lastErr
}
// getCloudFlareTLSConfiguration returns an accepted client TLS configuration to not get detected by CloudFlare directly
// in case the configuration needs to be updated later on: https://wiki.mozilla.org/Security/Server_Side_TLS .
func getCloudFlareTLSConfiguration() *tls.Config {
return &tls.Config{
CurvePreferences: []tls.CurveID{tls.CurveP256, tls.CurveP384, tls.CurveP521, tls.X25519},
}
}
// GetDefaultOptions returns the options set by default
func GetDefaultOptions() Options {
return Options{
AddMissingHeaders: true,
Headers: map[string]string{
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"User-Agent": GetRandomUserAgent(),
},
}
}