From 01ba9a0042aecada7a18212b4442d13ab428a85a Mon Sep 17 00:00:00 2001 From: sstent Date: Fri, 5 Sep 2025 11:59:36 -0700 Subject: [PATCH] sync authfix --- auth.go | 132 +++++++++++---- cloudflare.go | 157 ++++++++++++++++++ .../oauth1_response_1757093254543542353.html | Bin 0 -> 3599 bytes .../oauth1_response_1757093255905112264.html | Bin 0 -> 3601 bytes .../oauth1_response_1757093301799755076.html | Bin 0 -> 3614 bytes 5 files changed, 255 insertions(+), 34 deletions(-) create mode 100644 cloudflare.go create mode 100644 debug/oauth1_response_1757093254543542353.html create mode 100644 debug/oauth1_response_1757093255905112264.html create mode 100644 debug/oauth1_response_1757093301799755076.html diff --git a/auth.go b/auth.go index fbb3199..3be2c6b 100644 --- a/auth.go +++ b/auth.go @@ -14,6 +14,7 @@ import ( "math/rand" "net/http" "net/url" + "os" "regexp" "sort" "strings" @@ -37,11 +38,23 @@ func NewAuthenticator(opts ClientOptions) Authenticator { if opts.Domain == "" { opts.Domain = "garmin.com" } + + // Enhanced transport with better TLS settings and compression baseTransport := &http.Transport{ TLSClientConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, }, - Proxy: http.ProxyFromEnvironment, + Proxy: http.ProxyFromEnvironment, + DisableCompression: false, // Enable compression + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, } jar, err := cookiejar.New(nil) @@ -57,6 +70,15 @@ func NewAuthenticator(opts ClientOptions) Authenticator { if len(via) >= 10 { return errors.New("stopped after 10 redirects") } + // Preserve headers during redirects + if len(via) > 0 { + for key, values := range via[0].Header { + if key == "Authorization" || key == "Cookie" { + continue // Let the jar handle cookies + } + req.Header[key] = values + } + } if jar != nil { for _, v := range via { if v.Response != nil { @@ -74,7 +96,7 @@ func NewAuthenticator(opts ClientOptions) Authenticator { client: client, tokenURL: opts.TokenURL, storage: opts.Storage, - userAgent: "GCMv3", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", // More realistic user agent domain: opts.Domain, } @@ -85,7 +107,37 @@ func NewAuthenticator(opts ClientOptions) Authenticator { return auth } +// Enhanced browser headers to bypass Cloudflare +func (a *GarthAuthenticator) getRealisticBrowserHeaders(referer string) http.Header { + headers := http.Header{ + "User-Agent": {a.userAgent}, + "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}, + "Accept-Language": {"en-US,en;q=0.9"}, + "Accept-Encoding": {"gzip, deflate, br"}, + "Cache-Control": {"max-age=0"}, + "Sec-Ch-Ua": {`"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"`}, + "Sec-Ch-Ua-Mobile": {"?0"}, + "Sec-Ch-Ua-Platform": {`"Windows"`}, + "Sec-Fetch-Dest": {"document"}, + "Sec-Fetch-Mode": {"navigate"}, + "Sec-Fetch-Site": {"none"}, + "Sec-Fetch-User": {"?1"}, + "Upgrade-Insecure-Requests": {"1"}, + "DNT": {"1"}, + } + + if referer != "" { + headers.Set("Referer", referer) + headers.Set("Sec-Fetch-Site", "same-origin") + } + + return headers +} + func (a *GarthAuthenticator) Login(ctx context.Context, username, password, mfaToken string) (*Token, error) { + // Add delay to simulate human behavior + time.Sleep(time.Duration(500+rand.Intn(1000)) * time.Millisecond) + // Step 1: Get login ticket (lt) from SSO signin page authToken, tokenType, err := a.getLoginTicket(ctx) if err != nil { @@ -188,7 +240,8 @@ func (a *GarthAuthenticator) authorizeOAuth1Token(ctx context.Context, token *OA return fmt.Errorf("failed to create authorization request: %w", err) } - req.Header = a.getEnhancedBrowserHeaders(authURL) + // Use realistic browser headers + req.Header = a.getRealisticBrowserHeaders("https://connect.garmin.com") resp, err := a.client.Do(req) if err != nil { @@ -364,11 +417,13 @@ func (a *GarthAuthenticator) getLoginTicket(ctx context.Context) (string, string return "", "", fmt.Errorf("failed to create login page request: %w", err) } - req.Header.Set("User-Agent", a.userAgent) - req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - req.Header.Set("Accept-Language", "en-US,en;q=0.9") - req.Header.Set("Referer", fmt.Sprintf("https://sso.%s/sso/signin?id=gauth-widget&embedWidget=true&gauthHost=https://sso.%s/sso", a.domain, a.domain)) - req.Header.Set("Origin", fmt.Sprintf("https://sso.%s", a.domain)) + // Use realistic browser headers with proper referer chain + for key, values := range a.getRealisticBrowserHeaders("") { + req.Header[key] = values + } + + // Add some randomness to the request timing + time.Sleep(time.Duration(100+rand.Intn(200)) * time.Millisecond) resp, err := a.client.Do(req) if err != nil { @@ -376,6 +431,11 @@ func (a *GarthAuthenticator) getLoginTicket(ctx context.Context) (string, string } defer resp.Body.Close() + if resp.StatusCode == 403 { + // Cloudflare blocked us, try with different headers + return "", "", fmt.Errorf("blocked by Cloudflare - try using different IP or wait before retrying") + } + body, err := io.ReadAll(resp.Body) if err != nil { return "", "", fmt.Errorf("failed to read login page response: %w", err) @@ -416,6 +476,11 @@ func (a *GarthAuthenticator) authenticate(ctx context.Context, username, passwor req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", a.userAgent) + req.Header.Set("Referer", fmt.Sprintf("https://sso.%s/sso/signin", a.domain)) + req.Header.Set("Origin", fmt.Sprintf("https://sso.%s", a.domain)) + + // Add some delay to simulate typing + time.Sleep(time.Duration(800+rand.Intn(400)) * time.Millisecond) resp, err := a.client.Do(req) if err != nil { @@ -466,14 +531,12 @@ func (a *GarthAuthenticator) ExchangeToken(ctx context.Context, token *OAuth1Tok return a.exchangeOAuth1ForOAuth2Token(ctx, token) } -// Removed exchangeTicketForToken method - no longer needed - func (a *GarthAuthenticator) getEnhancedBrowserHeaders(referrer string) http.Header { u, _ := url.Parse(referrer) origin := fmt.Sprintf("%s://%s", u.Scheme, u.Host) return http.Header{ - "User-Agent": {"GCMv3"}, + "User-Agent": {a.userAgent}, "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"}, "Accept-Language": {"en-US,en;q=0.9"}, "Connection": {"keep-alive"}, @@ -490,34 +553,35 @@ func (a *GarthAuthenticator) getEnhancedBrowserHeaders(referrer string) http.Hea } func getCSRFToken(html string) (string, string, error) { - ltPatterns := []string{ - `name="lt"\s+value="([^"]+)"`, - `name="lt"\s+type="hidden"\s+value="([^"]+)"`, - `]*name="lt"[^>]*value="([^"]+)"[^>]*>`, + // Extract login ticket (lt) from hidden input field + re := regexp.MustCompile(``) + matches := re.FindStringSubmatch(html) + if len(matches) > 1 { + return matches[1], "lt", nil } - for _, pattern := range ltPatterns { - re := regexp.MustCompile(pattern) - matches := re.FindStringSubmatch(html) - if len(matches) > 1 { - return matches[1], "lt", nil - } + // Extract CSRF token as fallback + re = regexp.MustCompile(``) + matches = re.FindStringSubmatch(html) + if len(matches) > 1 { + return matches[1], "_csrf", nil } - csrfPatterns := []string{ - `name="_csrf"\s+value="([^"]+)"`, - `"csrfToken":"([^"]+)"`, + // Try alternative CSRF token pattern + re = regexp.MustCompile(`"csrfToken":"([^"]+)"`) + matches = re.FindStringSubmatch(html) + if len(matches) > 1 { + return matches[1], "_csrf", nil } - for _, pattern := range csrfPatterns { - re := regexp.MustCompile(pattern) - matches := re.FindStringSubmatch(html) - if len(matches) > 1 { - return matches[1], "_csrf", nil - } + // If we get here, we didn't find a token + // Log and save the response for debugging + log.Printf("Failed to find authentication token in HTML response") + debugFilename := fmt.Sprintf("debug/oauth1_response_%d.html", time.Now().UnixNano()) + if err := os.WriteFile(debugFilename, []byte(html), 0644); err != nil { + log.Printf("Failed to write debug file: %v", err) } - - return "", "", errors.New("no authentication token found") + return "", "", fmt.Errorf("no authentication token found in HTML response; response written to %s", debugFilename) } // RefreshToken implements token refresh functionality @@ -592,7 +656,7 @@ func (a *GarthAuthenticator) handleMFA(ctx context.Context, username, password, req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") - req.Header.Set("User-Agent", "GCMv3") + req.Header.Set("User-Agent", a.userAgent) resp, err := a.client.Do(req) if err != nil { diff --git a/cloudflare.go b/cloudflare.go new file mode 100644 index 0000000..7a3d755 --- /dev/null +++ b/cloudflare.go @@ -0,0 +1,157 @@ +package garth + +import ( + "context" + "fmt" + "io" + "math/rand" + "net/http" + "time" +) + +// CloudflareBypass provides methods to handle Cloudflare protection +type CloudflareBypass struct { + client *http.Client + userAgent string + maxRetries int +} + +// NewCloudflareBypass creates a new CloudflareBypass instance +func NewCloudflareBypass(client *http.Client) *CloudflareBypass { + return &CloudflareBypass{ + client: client, + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + maxRetries: 3, + } +} + +// MakeRequest performs a request with Cloudflare bypass techniques +func (cf *CloudflareBypass) MakeRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + var resp *http.Response + var err error + + for attempt := 0; attempt < cf.maxRetries; attempt++ { + // Clone the request to avoid modifying the original + clonedReq := cf.cloneRequest(req) + + // Apply bypass headers + cf.applyBypassHeaders(clonedReq) + + // Add random delay to simulate human behavior + if attempt > 0 { + delay := time.Duration(1000+rand.Intn(2000)) * time.Millisecond + time.Sleep(delay) + } + + resp, err = cf.client.Do(clonedReq) + if err != nil { + continue + } + + // Check if we got blocked by Cloudflare + if cf.isCloudflareBlocked(resp) { + resp.Body.Close() + if attempt < cf.maxRetries-1 { + // Try different user agent on retry + cf.rotateUserAgent() + continue + } + return nil, fmt.Errorf("blocked by Cloudflare after %d attempts", cf.maxRetries) + } + + return resp, nil + } + + return nil, err +} + +// applyBypassHeaders adds headers to bypass Cloudflare +func (cf *CloudflareBypass) applyBypassHeaders(req *http.Request) { + req.Header.Set("User-Agent", cf.userAgent) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + req.Header.Set("Accept-Language", "en-US,en;q=0.9") + req.Header.Set("Accept-Encoding", "gzip, deflate, br") + req.Header.Set("Cache-Control", "max-age=0") + req.Header.Set("Sec-Ch-Ua", `"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"`) + req.Header.Set("Sec-Ch-Ua-Mobile", "?0") + req.Header.Set("Sec-Ch-Ua-Platform", `"Windows"`) + req.Header.Set("Sec-Fetch-Dest", "document") + req.Header.Set("Sec-Fetch-Mode", "navigate") + req.Header.Set("Sec-Fetch-Site", "none") + req.Header.Set("Sec-Fetch-User", "?1") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("DNT", "1") + + // Add some randomized headers + if rand.Float32() < 0.7 { + req.Header.Set("Pragma", "no-cache") + } + if rand.Float32() < 0.5 { + req.Header.Set("Connection", "keep-alive") + } +} + +// isCloudflareBlocked checks if the response indicates Cloudflare blocking +func (cf *CloudflareBypass) isCloudflareBlocked(resp *http.Response) bool { + if resp.StatusCode == 403 { + // Check for Cloudflare-specific headers or content + if resp.Header.Get("Server") == "cloudflare" { + return true + } + if resp.Header.Get("CF-Ray") != "" { + return true + } + + // Check response body for Cloudflare indicators + if resp.ContentLength > 0 && resp.ContentLength < 50000 { + body, err := io.ReadAll(resp.Body) + if err == nil { + bodyStr := string(body) + if contains(bodyStr, "cloudflare") || contains(bodyStr, "Attention Required") { + return true + } + } + } + } + return false +} + +// rotateUserAgent changes the user agent for retry attempts +func (cf *CloudflareBypass) rotateUserAgent() { + userAgents := []string{ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0", + } + cf.userAgent = userAgents[rand.Intn(len(userAgents))] +} + +// cloneRequest creates a copy of the HTTP request +func (cf *CloudflareBypass) cloneRequest(req *http.Request) *http.Request { + cloned := req.Clone(req.Context()) + if cloned.Header == nil { + cloned.Header = make(http.Header) + } + return cloned +} + +// contains is a case-insensitive string contains check +func contains(s, substr string) bool { + return len(s) >= len(substr) && + (s == substr || + len(s) > len(substr) && + (s[:len(substr)] == substr || + s[len(s)-len(substr):] == substr || + containsAt(s, substr))) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/debug/oauth1_response_1757093254543542353.html b/debug/oauth1_response_1757093254543542353.html new file mode 100644 index 0000000000000000000000000000000000000000..5154463a34d47192bd000e7ed6c759acdd42f6de GIT binary patch literal 3599 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 k2mcMdgFlVg9$#L1Am0jhbI