package cli import ( "fmt" "net" "net/http" "time" ) // AppCallback provides hooks for injecting certain functionalities // from mobile platforms to main ctrld cli. // This allows mobile applications to customize behavior without modifying core CLI code type AppCallback struct { HostName func() string LanIp func() string MacAddress func() string Exit func(error string) } // AppConfig allows overwriting ctrld cli flags from mobile platforms. // This provides a clean interface for mobile apps to configure ctrld behavior type AppConfig struct { CdUID string ProvisionID string CustomHostname string HomeDir string UpstreamProto string Verbose int LogPath string } // Network and HTTP configuration constants const ( // defaultHTTPTimeout provides reasonable timeout for HTTP operations // This prevents hanging requests while allowing sufficient time for network delays defaultHTTPTimeout = 30 * time.Second // defaultMaxRetries provides retry attempts for failed HTTP requests // This improves reliability in unstable network conditions defaultMaxRetries = 3 // downloadServerIp is the fallback IP for download operations // This ensures downloads work even when DNS resolution fails downloadServerIp = "23.171.240.151" ) // httpClientWithFallback returns an HTTP client configured with timeout and IPv4 fallback // This ensures reliable HTTP operations by preferring IPv4 and handling timeouts gracefully func httpClientWithFallback(timeout time.Duration) *http.Client { return &http.Client{ Timeout: timeout, Transport: &http.Transport{ // Prefer IPv4 over IPv6 // This improves compatibility with networks that have IPv6 issues DialContext: (&net.Dialer{ Timeout: 10 * time.Second, KeepAlive: 30 * time.Second, FallbackDelay: 1 * time.Millisecond, // Very small delay to prefer IPv4 }).DialContext, }, } } // doWithRetry performs an HTTP request with retries // This improves reliability by automatically retrying failed requests with exponential backoff func doWithRetry(req *http.Request, maxRetries int, ip string) (*http.Response, error) { return doWithRetryClient(httpClientWithFallback(defaultHTTPTimeout), req, maxRetries, ip) } // doWithRetryClient is doWithRetry with an injectable client, so the retry and // error-composition behaviour can be tested without real network access. func doWithRetryClient(client *http.Client, req *http.Request, maxRetries int, ip string) (*http.Response, error) { var lastErr error var ipReq *http.Request if ip != "" { ipReq = req.Clone(req.Context()) ipReq.Host = ip ipReq.URL.Host = ip } for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { // Linear backoff reduces server load and improves success rate time.Sleep(time.Second * time.Duration(attempt+1)) } resp, err := client.Do(req) if err == nil { return resp, nil } // Keep the hostname attempt's error: it carries the diagnosis (on Windows, // a local firewall denying the socket shows up here as WSAEACCES), while the // direct-ip fallback often fails for an unrelated reason such as an // unreachable IPv6 route. attemptErr := err if ipReq != nil { mainLog.Load().Warn().Err(err).Msgf("Dial to %q failed", req.Host) mainLog.Load().Warn().Msgf("Fallback to direct ip to download prod version: %q", ip) resp, fallbackErr := client.Do(ipReq) if fallbackErr == nil { return resp, nil } attemptErr = fmt.Errorf("%w; fallback to direct ip %s failed: %w", attemptErr, ip, fallbackErr) } lastErr = attemptErr mainLog.Load().Debug().Err(attemptErr). Str("method", req.Method). Str("url", req.URL.String()). Msgf("HTTP request attempt %d/%d failed", attempt+1, maxRetries) } return nil, fmt.Errorf("failed after %d attempts to %s %s: %w", maxRetries, req.Method, req.URL, lastErr) } // Helper for making GET requests with retries // This provides a simplified interface for common GET operations with built-in retry logic func getWithRetry(url string, ip string) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } return doWithRetry(req, defaultMaxRetries, ip) }