131 lines
3.3 KiB
Go
131 lines
3.3 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/debridmediamanager.com/zurg/internal/config"
|
|
cmap "github.com/orcaman/concurrent-map/v2"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type HTTPClient struct {
|
|
Client *http.Client
|
|
MaxRetries int
|
|
Backoff func(attempt int) time.Duration
|
|
ShouldRetry func(resp *http.Response, err error) int
|
|
BearerToken string
|
|
log *zap.SugaredLogger
|
|
config config.ConfigInterface
|
|
IPv6 cmap.ConcurrentMap[string, net.IP]
|
|
}
|
|
|
|
func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|
req.Close = true
|
|
if r.config != nil && strings.Contains(req.Host, "download.real-debrid.") {
|
|
prefHost := r.config.GetRandomPreferredHost()
|
|
if prefHost != "" {
|
|
req.URL.Host = prefHost
|
|
}
|
|
}
|
|
if r.BearerToken != "" {
|
|
req.Header.Set("Authorization", "Bearer "+r.BearerToken)
|
|
}
|
|
|
|
if r.config.ShouldForceIPv6() {
|
|
dialer := &net.Dialer{}
|
|
transport := &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
host, port, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ipv6, ok := r.IPv6.Get(host); !ok {
|
|
// Lookup IP address if not found in map
|
|
ips, err := net.LookupIP(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ip := range ips {
|
|
if ip.To4() == nil { // IPv6
|
|
ipv6 = ip
|
|
r.IPv6.Set(host, ipv6)
|
|
break
|
|
}
|
|
}
|
|
if ipv6 == nil { // No IPv6 address found, fallback to default
|
|
addr = net.JoinHostPort(host, port)
|
|
} else {
|
|
addr = net.JoinHostPort(ipv6.String(), port)
|
|
}
|
|
} else if ipv6 != nil && host == req.URL.Hostname() {
|
|
addr = net.JoinHostPort(ipv6.String(), port)
|
|
}
|
|
return dialer.DialContext(ctx, network, addr)
|
|
},
|
|
}
|
|
r.Client.Transport = transport
|
|
}
|
|
|
|
var resp *http.Response
|
|
var err error
|
|
attempt := 0
|
|
for {
|
|
resp, err = r.Client.Do(req)
|
|
if val := r.ShouldRetry(resp, err); val == -1 {
|
|
return resp, err
|
|
} else {
|
|
if val == 0 {
|
|
time.Sleep(8 * time.Second) // extra delay
|
|
} else {
|
|
attempt += val
|
|
if attempt > r.MaxRetries {
|
|
return resp, err
|
|
}
|
|
time.Sleep(r.Backoff(attempt))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewHTTPClient(token string, maxRetries int, timeoutSecs int, cfg config.ConfigInterface, log *zap.SugaredLogger) *HTTPClient {
|
|
return &HTTPClient{
|
|
BearerToken: token,
|
|
Client: &http.Client{
|
|
Timeout: time.Duration(timeoutSecs) * time.Second,
|
|
},
|
|
MaxRetries: maxRetries,
|
|
Backoff: func(attempt int) time.Duration {
|
|
maxDuration := 60
|
|
backoff := int(math.Pow(2, float64(attempt)))
|
|
if backoff > maxDuration {
|
|
backoff = maxDuration
|
|
}
|
|
return time.Duration(backoff) * time.Second
|
|
},
|
|
ShouldRetry: func(resp *http.Response, err error) int {
|
|
if resp != nil {
|
|
if resp.StatusCode == 429 || resp.StatusCode == 400 {
|
|
return 0 // retry but don't increment attempt
|
|
}
|
|
return -1 // don't retry
|
|
} else if err != nil {
|
|
errStr := err.Error()
|
|
if strings.Contains(errStr, "EOF") || strings.Contains(errStr, "connection reset") || strings.Contains(errStr, "no such host") {
|
|
return 0 // retry but don't increment attempt
|
|
} else {
|
|
return 1
|
|
}
|
|
}
|
|
return 1 // retry and increment attempt
|
|
},
|
|
log: log,
|
|
config: cfg,
|
|
IPv6: cmap.New[net.IP](),
|
|
}
|
|
}
|