diff --git a/go_backend/httputil.go b/go_backend/httputil.go index c62b2cf..be0f75d 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -1,7 +1,6 @@ package gobackend import ( - "context" "crypto/tls" "errors" "fmt" @@ -12,12 +11,8 @@ import ( "net/url" "strconv" "strings" - "sync" "syscall" "time" - - utls "github.com/refraction-networking/utls" - "golang.org/x/net/http2" ) // getRandomUserAgent generates a random Windows Chrome User-Agent string @@ -43,6 +38,7 @@ const ( SongLinkTimeout = 30 * time.Second DefaultMaxRetries = 3 DefaultRetryDelay = 1 * time.Second + Second = time.Second // Exported for use in other files ) // Shared transport with connection pooling to prevent TCP exhaustion @@ -64,96 +60,6 @@ var sharedTransport = &http.Transport{ DisableCompression: true, } -// uTLS transport that mimics Chrome's TLS fingerprint to bypass Cloudflare -// Uses HTTP/2 for optimal performance as uTLS works best with HTTP/2 -type utlsTransport struct { - dialer *net.Dialer - mu sync.Mutex - h2Transports map[string]*http2.Transport -} - -func newUTLSTransport() *utlsTransport { - return &utlsTransport{ - dialer: &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }, - h2Transports: make(map[string]*http2.Transport), - } -} - -func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // For non-HTTPS, use standard transport - if req.URL.Scheme != "https" { - return sharedTransport.RoundTrip(req) - } - - host := req.URL.Hostname() - port := t.getPort(req.URL) - addr := net.JoinHostPort(host, port) - - // Dial TCP connection - conn, err := t.dialer.DialContext(req.Context(), "tcp", addr) - if err != nil { - return nil, err - } - - // Create uTLS connection with Chrome fingerprint (supports HTTP/2 ALPN) - tlsConn := utls.UClient(conn, &utls.Config{ - ServerName: host, - NextProtos: []string{"h2", "http/1.1"}, // Prefer HTTP/2 - }, utls.HelloChrome_Auto) - - // Perform TLS handshake - if err := tlsConn.Handshake(); err != nil { - conn.Close() - return nil, err - } - - // Check if server supports HTTP/2 - negotiatedProto := tlsConn.ConnectionState().NegotiatedProtocol - - if negotiatedProto == "h2" { - // Use HTTP/2 transport - h2Transport := &http2.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { - return tlsConn, nil - }, - AllowHTTP: false, - DisableCompression: false, - } - return h2Transport.RoundTrip(req) - } - - // Fallback to HTTP/1.1 - transport := &http.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return tlsConn, nil - }, - DisableKeepAlives: true, - } - - return transport.RoundTrip(req) -} - -func (t *utlsTransport) getPort(u *url.URL) string { - if u.Port() != "" { - return u.Port() - } - if u.Scheme == "https" { - return "443" - } - return "80" -} - -// Cloudflare bypass client using uTLS Chrome fingerprint -var cloudflareBypassTransport = newUTLSTransport() - -var cloudflareBypassClient = &http.Client{ - Transport: cloudflareBypassTransport, - Timeout: DefaultTimeout, -} - var sharedClient = &http.Client{ Transport: sharedTransport, Timeout: DefaultTimeout, @@ -179,12 +85,6 @@ func GetDownloadClient() *http.Client { return downloadClient } -// GetCloudflareBypassClient returns an HTTP client that mimics Chrome's TLS fingerprint -// Use this when requests are blocked by Cloudflare (common when using VPN) -func GetCloudflareBypassClient() *http.Client { - return cloudflareBypassClient -} - // CloseIdleConnections closes idle connections in the shared transport func CloseIdleConnections() { sharedTransport.CloseIdleConnections() @@ -200,81 +100,6 @@ func DoRequestWithUserAgent(client *http.Client, req *http.Request) (*http.Respo return resp, err } -// DoRequestWithCloudflareBypass attempts request with standard client first, -// then retries with uTLS Chrome fingerprint if Cloudflare blocks it. -// This is useful when using VPN as Cloudflare detects Go's default TLS fingerprint. -func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { - req.Header.Set("User-Agent", getRandomUserAgent()) - - // Try with standard client first - resp, err := sharedClient.Do(req) - if err == nil { - // Check for Cloudflare challenge page (403 with specific markers) - if resp.StatusCode == 403 || resp.StatusCode == 503 { - body, readErr := io.ReadAll(resp.Body) - resp.Body.Close() - - if readErr == nil { - bodyStr := strings.ToLower(string(body)) - cloudflareMarkers := []string{ - "cloudflare", "cf-ray", "checking your browser", - "please wait", "ddos protection", "ray id", - "enable javascript", "challenge-platform", - } - - isCloudflare := false - for _, marker := range cloudflareMarkers { - if strings.Contains(bodyStr, marker) { - isCloudflare = true - break - } - } - - if isCloudflare { - LogDebug("HTTP", "Cloudflare detected, retrying with Chrome TLS fingerprint...") - - // Clone request for retry - reqCopy := req.Clone(req.Context()) - reqCopy.Header.Set("User-Agent", getRandomUserAgent()) - - // Retry with uTLS Chrome fingerprint - return cloudflareBypassClient.Do(reqCopy) - } - } - - // Not Cloudflare, return original response (recreate body) - return &http.Response{ - Status: resp.Status, - StatusCode: resp.StatusCode, - Header: resp.Header, - Body: io.NopCloser(strings.NewReader(string(body))), - }, nil - } - return resp, nil - } - - // Check if error might be TLS-related (Cloudflare blocking) - errStr := strings.ToLower(err.Error()) - tlsRelated := strings.Contains(errStr, "tls") || - strings.Contains(errStr, "handshake") || - strings.Contains(errStr, "certificate") || - strings.Contains(errStr, "connection reset") - - if tlsRelated { - LogDebug("HTTP", "TLS error detected, retrying with Chrome TLS fingerprint: %v", err) - - // Clone request for retry - reqCopy := req.Clone(req.Context()) - reqCopy.Header.Set("User-Agent", getRandomUserAgent()) - - // Retry with uTLS Chrome fingerprint - return cloudflareBypassClient.Do(reqCopy) - } - - CheckAndLogISPBlocking(err, req.URL.String(), "HTTP") - return nil, err -} - // RetryConfig holds configuration for retry logic type RetryConfig struct { MaxRetries int diff --git a/go_backend/httputil_ios.go b/go_backend/httputil_ios.go new file mode 100644 index 0000000..3302ad0 --- /dev/null +++ b/go_backend/httputil_ios.go @@ -0,0 +1,27 @@ +//go:build ios + +package gobackend + +import ( + "net/http" +) + +// iOS version: uTLS is not supported on iOS due to cgo DNS resolver issues +// Fall back to standard HTTP client + +// GetCloudflareBypassClient returns the standard HTTP client on iOS +// uTLS is not available on iOS due to cgo DNS resolver compatibility issues +func GetCloudflareBypassClient() *http.Client { + return sharedClient +} + +// DoRequestWithCloudflareBypass on iOS just uses the standard client +// uTLS Chrome fingerprint bypass is not available on iOS +func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { + req.Header.Set("User-Agent", getRandomUserAgent()) + resp, err := sharedClient.Do(req) + if err != nil { + CheckAndLogISPBlocking(err, req.URL.String(), "HTTP") + } + return resp, err +} diff --git a/go_backend/httputil_utls.go b/go_backend/httputil_utls.go new file mode 100644 index 0000000..ddf7185 --- /dev/null +++ b/go_backend/httputil_utls.go @@ -0,0 +1,188 @@ +//go:build !ios + +package gobackend + +import ( + "context" + "crypto/tls" + "io" + "net" + "net/http" + "net/url" + "strings" + "sync" + + utls "github.com/refraction-networking/utls" + "golang.org/x/net/http2" +) + +// uTLS transport that mimics Chrome's TLS fingerprint to bypass Cloudflare +// Uses HTTP/2 for optimal performance as uTLS works best with HTTP/2 +type utlsTransport struct { + dialer *net.Dialer + mu sync.Mutex + h2Transports map[string]*http2.Transport +} + +func newUTLSTransport() *utlsTransport { + return &utlsTransport{ + dialer: &net.Dialer{ + Timeout: 30 * Second, + KeepAlive: 30 * Second, + }, + h2Transports: make(map[string]*http2.Transport), + } +} + +func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // For non-HTTPS, use standard transport + if req.URL.Scheme != "https" { + return sharedTransport.RoundTrip(req) + } + + host := req.URL.Hostname() + port := t.getPort(req.URL) + addr := net.JoinHostPort(host, port) + + // Dial TCP connection + conn, err := t.dialer.DialContext(req.Context(), "tcp", addr) + if err != nil { + return nil, err + } + + // Create uTLS connection with Chrome fingerprint (supports HTTP/2 ALPN) + tlsConn := utls.UClient(conn, &utls.Config{ + ServerName: host, + NextProtos: []string{"h2", "http/1.1"}, // Prefer HTTP/2 + }, utls.HelloChrome_Auto) + + // Perform TLS handshake + if err := tlsConn.Handshake(); err != nil { + conn.Close() + return nil, err + } + + // Check if server supports HTTP/2 + negotiatedProto := tlsConn.ConnectionState().NegotiatedProtocol + + if negotiatedProto == "h2" { + // Use HTTP/2 transport + h2Transport := &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + return tlsConn, nil + }, + AllowHTTP: false, + DisableCompression: false, + } + return h2Transport.RoundTrip(req) + } + + // Fallback to HTTP/1.1 + transport := &http.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return tlsConn, nil + }, + DisableKeepAlives: true, + } + + return transport.RoundTrip(req) +} + +func (t *utlsTransport) getPort(u *url.URL) string { + if u.Port() != "" { + return u.Port() + } + if u.Scheme == "https" { + return "443" + } + return "80" +} + +// Cloudflare bypass client using uTLS Chrome fingerprint +var cloudflareBypassTransport = newUTLSTransport() + +var cloudflareBypassClient = &http.Client{ + Transport: cloudflareBypassTransport, + Timeout: DefaultTimeout, +} + +// GetCloudflareBypassClient returns an HTTP client that mimics Chrome's TLS fingerprint +// Use this when requests are blocked by Cloudflare (common when using VPN) +func GetCloudflareBypassClient() *http.Client { + return cloudflareBypassClient +} + +// DoRequestWithCloudflareBypass attempts request with standard client first, +// then retries with uTLS Chrome fingerprint if Cloudflare blocks it. +// This is useful when using VPN as Cloudflare detects Go's default TLS fingerprint. +func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { + req.Header.Set("User-Agent", getRandomUserAgent()) + + // Try with standard client first + resp, err := sharedClient.Do(req) + if err == nil { + // Check for Cloudflare challenge page (403 with specific markers) + if resp.StatusCode == 403 || resp.StatusCode == 503 { + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + + if readErr == nil { + bodyStr := strings.ToLower(string(body)) + cloudflareMarkers := []string{ + "cloudflare", "cf-ray", "checking your browser", + "please wait", "ddos protection", "ray id", + "enable javascript", "challenge-platform", + } + + isCloudflare := false + for _, marker := range cloudflareMarkers { + if strings.Contains(bodyStr, marker) { + isCloudflare = true + break + } + } + + if isCloudflare { + LogDebug("HTTP", "Cloudflare detected, retrying with Chrome TLS fingerprint...") + + // Clone request for retry + reqCopy := req.Clone(req.Context()) + reqCopy.Header.Set("User-Agent", getRandomUserAgent()) + + // Retry with uTLS Chrome fingerprint + return cloudflareBypassClient.Do(reqCopy) + } + } + + // Not Cloudflare, return original response (recreate body) + return &http.Response{ + Status: resp.Status, + StatusCode: resp.StatusCode, + Header: resp.Header, + Body: io.NopCloser(strings.NewReader(string(body))), + }, nil + } + return resp, nil + } + + // Check if error might be TLS-related (Cloudflare blocking) + errStr := strings.ToLower(err.Error()) + tlsRelated := strings.Contains(errStr, "tls") || + strings.Contains(errStr, "handshake") || + strings.Contains(errStr, "certificate") || + strings.Contains(errStr, "connection reset") + + if tlsRelated { + LogDebug("HTTP", "TLS error detected, retrying with Chrome TLS fingerprint: %v", err) + + // Clone request for retry + reqCopy := req.Clone(req.Context()) + reqCopy.Header.Set("User-Agent", getRandomUserAgent()) + + // Retry with uTLS Chrome fingerprint + return cloudflareBypassClient.Do(reqCopy) + } + + CheckAndLogISPBlocking(err, req.URL.String(), "HTTP") + return nil, err +}