sync authfix

This commit is contained in:
2025-09-05 11:59:36 -07:00
parent 0bb1b03d9c
commit 01ba9a0042
5 changed files with 255 additions and 34 deletions

132
auth.go
View File

@@ -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="([^"]+)"`,
`<input[^>]*name="lt"[^>]*value="([^"]+)"[^>]*>`,
// Extract login ticket (lt) from hidden input field
re := regexp.MustCompile(`<input\s+type="hidden"\s+name="lt"\s+value="([^"]+)"\s*/>`)
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(`<input\s+type="hidden"\s+name="_csrf"\s+value="([^"]+)"\s*/>`)
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 {

157
cloudflare.go Normal file
View File

@@ -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
}

Binary file not shown.

Binary file not shown.

Binary file not shown.