mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 16:42:28 +00:00
sync authfix
This commit is contained in:
132
auth.go
132
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="([^"]+)"`,
|
||||
`<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
157
cloudflare.go
Normal 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
|
||||
}
|
||||
BIN
debug/oauth1_response_1757093254543542353.html
Normal file
BIN
debug/oauth1_response_1757093254543542353.html
Normal file
Binary file not shown.
BIN
debug/oauth1_response_1757093255905112264.html
Normal file
BIN
debug/oauth1_response_1757093255905112264.html
Normal file
Binary file not shown.
BIN
debug/oauth1_response_1757093301799755076.html
Normal file
BIN
debug/oauth1_response_1757093301799755076.html
Normal file
Binary file not shown.
Reference in New Issue
Block a user