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(), }, } }