package utils import ( "context" "net" "net/http" "net/url" "os" "strings" "time" ) // NewHTTPClientWithGoogleDNS creates an HTTP client that forces DNS resolution via 8.8.8.8 func NewHTTPClientWithGoogleDNS() *http.Client { dialer := &net.Dialer{ Resolver: &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{ Timeout: 10 * time.Second, } return d.DialContext(ctx, "udp", "8.8.8.8:53") }, }, } transport := &http.Transport{ DialContext: dialer.DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } return &http.Client{ Transport: transport, } } // NewIPv4OnlyHTTPClient creates an HTTP client that forces IPv4 only (disables IPv6) func NewIPv4OnlyHTTPClient() *http.Client { dialer := &net.Dialer{ Resolver: &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { // Force IPv4 by dialing 8.8.8.8:53 (Google DNS) d := net.Dialer{ Timeout: 10 * time.Second, } return d.DialContext(ctx, "udp", "8.8.8.8:53") }, }, } transport := &http.Transport{ DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { // Force IPv4 by replacing any IPv6 addresses with IPv4 lookup // If address contains colon (IPv6), resolve hostname separately and pick IPv4 if strings.Contains(address, ":") { // Extract host from "host:port" or "[ipv6]:port" host, port, err := net.SplitHostPort(address) if err != nil { return nil, err } // Resolve host to IPv4 addresses only addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host) if err != nil { return nil, err } // Find first IPv4 address for _, addr := range addrs { if addr.IP.To4() != nil { return dialer.DialContext(ctx, "tcp", net.JoinHostPort(addr.IP.String(), port)) } } return nil, &net.DNSError{Err: "no IPv4 address found for " + host} } // Already IPv4 or hostname, use default dialer return dialer.DialContext(ctx, "tcp", address) }, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } return &http.Client{ Transport: transport, } } // NewProxiedHTTPClient creates an HTTP client that uses a proxy if configured // It respects standard proxy environment variables and adds proxy support for HTTPS func NewProxiedHTTPClient() *http.Client { // Check for proxy configuration proxyURL := os.Getenv("TELEGRAM_PROXY_URL") if proxyURL == "" { proxyURL = os.Getenv("HTTPS_PROXY") if proxyURL == "" { proxyURL = os.Getenv("HTTP_PROXY") } } transport := &http.Transport{ ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } // If proxy is configured, set it up if proxyURL != "" { u, err := url.Parse(proxyURL) if err != nil { // If proxy URL is invalid, log and continue without proxy // In production, we'd use a logger but this is a low-level utils function // So we'll just proceed without proxy } else { transport.Proxy = http.ProxyURL(u) } } return &http.Client{ Transport: transport, } } // IsProxyOrNetworkError checks if an error indicates proxy or network policy block func IsProxyOrNetworkError(err error) bool { if err == nil { return false } errStr := err.Error() // Common network/block indicators indicators := []string{ "operation not permitted", "permission denied", "network is unreachable", "connection refused", "timeout", "no route to host", "i/o timeout", "dial tcp", "connect:", "proxy", } for _, indicator := range indicators { if strings.Contains(strings.ToLower(errStr), indicator) { return true } } return false }