mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-05 14:18:16 +02:00
feat(http): add uTLS Chrome fingerprint for Cloudflare bypass
- Added uTLS library to mimic Chrome's TLS fingerprint - Uses HTTP/2 for optimal performance with uTLS - Auto-detects Cloudflare challenge and retries with Chrome fingerprint - Helps VPN users bypass Cloudflare TLS fingerprint detection
This commit is contained in:
+7
-5
@@ -9,15 +9,17 @@ require (
|
||||
github.com/go-flac/flacpicture v0.3.0
|
||||
github.com/go-flac/flacvorbis v0.2.0
|
||||
github.com/go-flac/go-flac v1.0.0
|
||||
github.com/refraction-networking/utls v1.8.2
|
||||
golang.org/x/net v0.49.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
)
|
||||
|
||||
+14
-10
@@ -1,5 +1,7 @@
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM=
|
||||
@@ -14,15 +16,17 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 h1:Cr6kbEvA6nqvdHynE4CtVKlqpZB9dS1Jva/6IsHA19g=
|
||||
golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294/go.mod h1:RdZ+3sb4CVgpCFnzv+I4haEpwqFfsfzlLHs3L7ok+e0=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -11,8 +12,12 @@ 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
|
||||
@@ -59,6 +64,96 @@ 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,
|
||||
@@ -84,6 +179,12 @@ 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()
|
||||
@@ -99,6 +200,81 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user