Add network test

This commit is contained in:
Ben Adrian Sarmiento
2024-06-16 06:06:57 +02:00
parent b1427c2d63
commit e50806d8e1
4 changed files with 235 additions and 33 deletions

View File

@@ -52,6 +52,17 @@ func MainApp(configPath string) {
os.Exit(1) os.Exit(1)
} }
repo := http.NewIPRepository(log.Named("network_test"))
repoClient := http.NewHTTPClient(
"",
0,
1,
true,
config,
log.Named("network_test"),
)
repo.NetworkTest(repoClient, false)
apiClient := http.NewHTTPClient( apiClient := http.NewHTTPClient(
config.GetToken(), config.GetToken(),
config.GetRetriesUntilFailed(), // default retries = 2 config.GetRetriesUntilFailed(), // default retries = 2
@@ -74,7 +85,7 @@ func MainApp(configPath string) {
"", // no token required for download client "", // no token required for download client
config.GetRetriesUntilFailed(), // config.GetRetriesUntilFailed(), //
config.GetDownloadTimeoutSecs(), // config.GetDownloadTimeoutSecs(), //
true, // download client supports ipv6 true, // set as download client
config, config,
log.Named("download_client"), log.Named("download_client"),
) )

View File

@@ -258,9 +258,6 @@ func (t *TorrentManager) applyMediaInfoDetails(torrent *Torrent) {
func (t *TorrentManager) readTorrentFromFile(filePath string) *Torrent { func (t *TorrentManager) readTorrentFromFile(filePath string) *Torrent {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if os.IsNotExist(err) {
return nil
}
return nil return nil
} }
defer file.Close() defer file.Close()
@@ -318,9 +315,6 @@ func (t *TorrentManager) readInfoFromFile(torrentID string) *realdebrid.TorrentI
filePath := "data/info/" + torrentID + ".zurginfo" filePath := "data/info/" + torrentID + ".zurginfo"
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if os.IsNotExist(err) {
return nil
}
return nil return nil
} }
defer file.Close() defer file.Close()

View File

@@ -24,23 +24,18 @@ import (
) )
type HTTPClient struct { type HTTPClient struct {
client *http.Client client *http.Client
maxRetries int maxRetries int
timeoutSecs int timeoutSecs int
backoff func(attempt int) time.Duration backoff func(attempt int) time.Duration
bearerToken string bearerToken string
supportIPv6 bool isDownloadClient bool
cfg config.ConfigInterface cfg config.ConfigInterface
ipv6 cmap.ConcurrentMap[string, string] ipv6 cmap.ConcurrentMap[string, string]
ipv6Hosts []string ipv6Hosts []string
log *logutil.Logger log *logutil.Logger
} }
// {
// "error": "infringing_file",
// "error_code": 35
// }
type ApiErrorResponse struct { type ApiErrorResponse struct {
Message string `json:"error"` Message string `json:"error"`
Code int `json:"error_code"` Code int `json:"error_code"`
@@ -54,20 +49,20 @@ func NewHTTPClient(
token string, token string,
maxRetries int, maxRetries int,
timeoutSecs int, timeoutSecs int,
supportIPv6 bool, isDownloadClient bool,
cfg config.ConfigInterface, cfg config.ConfigInterface,
log *logutil.Logger, log *logutil.Logger,
) *HTTPClient { ) *HTTPClient {
client := HTTPClient{ client := HTTPClient{
bearerToken: token, bearerToken: token,
client: &http.Client{}, client: &http.Client{},
maxRetries: maxRetries, maxRetries: maxRetries,
timeoutSecs: timeoutSecs, timeoutSecs: timeoutSecs,
backoff: backoffFunc, backoff: backoffFunc,
supportIPv6: supportIPv6, isDownloadClient: isDownloadClient,
cfg: cfg, cfg: cfg,
ipv6: cmap.New[string](), ipv6: cmap.New[string](),
log: log, log: log,
} }
var dialer proxy.Dialer = &net.Dialer{ var dialer proxy.Dialer = &net.Dialer{
@@ -217,7 +212,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
func (r *HTTPClient) replaceWithIPv6Host(req *http.Request) { func (r *HTTPClient) replaceWithIPv6Host(req *http.Request) {
// don't replace host if IPv6 is not supported or not forced // don't replace host if IPv6 is not supported or not forced
if !r.supportIPv6 || !r.cfg.ShouldForceIPv6() { if !r.isDownloadClient || !r.cfg.ShouldForceIPv6() {
return return
} }
// this host should be replaced // this host should be replaced

202
pkg/http/ip.go Normal file
View File

@@ -0,0 +1,202 @@
package http
import (
"context"
"encoding/json"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"os"
"time"
"github.com/debridmediamanager/zurg/pkg/logutil"
)
type IPRepository struct {
ipv4 []string
ipv6 []string
latencyMap map[string]float64
log *logutil.Logger
}
func NewIPRepository(log *logutil.Logger) *IPRepository {
repo := &IPRepository{
ipv4: []string{},
ipv6: []string{},
latencyMap: make(map[string]float64),
log: log,
}
repo.lookupDomains()
return repo
}
func (r *IPRepository) NetworkTest(downloadClient *HTTPClient, forceRun bool) {
latencyFile := "data/latency.json"
if !forceRun {
latencyData := r.readLatencyFile(latencyFile)
if latencyData != nil {
r.latencyMap = *latencyData
return
}
}
r.log.Info("Network test will start now. Note that it will only run once and record the latency of each domain for future use.")
r.latencyTest(downloadClient)
r.log.Infof("Network test completed. Saving the results to %s", latencyFile)
r.writeLatencyFile(latencyFile)
}
func (r *IPRepository) lookupDomains() {
limit := 99
increment := 10
start := 0
for {
lastDomainWorked := false
for i := start; i <= limit; i++ {
domain := fmt.Sprintf("%d.download.real-debrid.com", i)
ips, err := net.LookupIP(domain)
if err == nil && len(ips) > 0 {
hasIPv6 := false
for _, ip := range ips {
if ip.To4() == nil {
hasIPv6 = true
}
}
// assume it always has ipv4
r.ipv4 = append(r.ipv4, domain)
if hasIPv6 {
r.ipv6 = append(r.ipv6, domain)
}
if i == limit {
lastDomainWorked = true
}
}
domain2 := fmt.Sprintf("%d.download.real-debrid.cloud", i)
ips2, err := net.LookupIP(domain2)
if err == nil && len(ips2) > 0 {
hasIPv6 := false
for _, ip := range ips {
if ip.To4() == nil {
hasIPv6 = true
}
}
r.ipv4 = append(r.ipv4, domain2)
if hasIPv6 {
r.ipv6 = append(r.ipv6, domain2)
}
if i == limit {
lastDomainWorked = true
}
}
}
if lastDomainWorked {
start = limit + 1
limit += increment
} else {
break
}
}
r.log.Infof("Found %d IPv4 domains and %d IPv6 domains", len(r.ipv4), len(r.ipv6))
}
func (r *IPRepository) latencyTest(downloadClient *HTTPClient) {
const testFileSize = 1
const iterations = 3
for _, domain := range r.ipv4 {
url := fmt.Sprintf("https://%s/speedtest/test.rar/%f", domain, rand.Float64())
var totalDuration float64
hasError := false
for i := 0; i < iterations; i++ {
ctx, cancel := context.WithTimeout(context.Background(), iterations*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
r.log.Warnf("Failed to create request for %s: %v", domain, err)
hasError = true
break
}
headers := make(http.Header)
headers.Set("Range", fmt.Sprintf("bytes=0-%d", testFileSize-1))
req.Header = headers
start := time.Now()
resp, err := downloadClient.Do(req)
if err != nil {
r.log.Warnf("Failed to download from %s: %v", domain, err)
hasError = true
break
}
limitedReader := io.LimitReader(resp.Body, testFileSize)
_, err = io.Copy(io.Discard, limitedReader)
resp.Body.Close()
if err != nil && err != io.EOF {
r.log.Warnf("Failed to read from %s: %v", domain, err)
hasError = true
break
}
duration := time.Since(start).Seconds()
totalDuration += duration
}
if hasError {
continue
}
r.latencyMap[domain] = totalDuration / 3
r.log.Debugf("Latency from %s: %.5f seconds", domain, r.latencyMap[domain])
}
}
func (r *IPRepository) readLatencyFile(latencyFile string) *map[string]float64 {
if _, err := os.Stat(latencyFile); err == nil {
file, err := os.Open(latencyFile)
if err != nil {
return nil
}
defer file.Close()
jsonData, err := io.ReadAll(file)
if err != nil {
return nil
}
var latencyMap map[string]float64
if err := json.Unmarshal(jsonData, &latencyMap); err != nil {
return nil
}
return &latencyMap
}
return nil
}
func (r *IPRepository) writeLatencyFile(latencyFile string) {
file, err := os.Create(latencyFile)
if err != nil {
r.log.Warnf("Cannot create latency file %s: %v", latencyFile, err)
return
}
defer file.Close()
jsonData, err := json.Marshal(r.latencyMap)
if err != nil {
r.log.Warnf("Cannot marshal latency map: %v", err)
return
}
if _, err := file.Write(jsonData); err != nil {
r.log.Warnf("Cannot write to latency file %s: %v", latencyFile, err)
return
}
}