mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 16:42:28 +00:00
159 lines
4.0 KiB
Go
159 lines
4.0 KiB
Go
package garth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// AuthTransport implements http.RoundTripper to inject authentication headers
|
|
type AuthTransport struct {
|
|
base http.RoundTripper
|
|
auth *GarthAuthenticator
|
|
storage TokenStorage
|
|
userAgent string
|
|
mutex sync.Mutex // Protects refreshing token
|
|
}
|
|
|
|
// NewAuthTransport creates a new authenticated transport with specified storage
|
|
func NewAuthTransport(auth *GarthAuthenticator, storage TokenStorage, base http.RoundTripper) *AuthTransport {
|
|
if base == nil {
|
|
base = http.DefaultTransport
|
|
}
|
|
|
|
if storage == nil {
|
|
storage = NewFileStorage("garmin_session.json")
|
|
}
|
|
|
|
return &AuthTransport{
|
|
base: base,
|
|
auth: auth,
|
|
storage: storage,
|
|
userAgent: "GarthClient/1.0",
|
|
}
|
|
}
|
|
|
|
// RoundTrip executes a single HTTP transaction with authentication
|
|
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
// Clone request to avoid modifying the original
|
|
req = cloneRequest(req)
|
|
|
|
// Get current token
|
|
token, err := t.storage.GetToken()
|
|
if err != nil {
|
|
return nil, &AuthError{
|
|
StatusCode: http.StatusUnauthorized,
|
|
Message: "Token not available",
|
|
Cause: err,
|
|
}
|
|
}
|
|
|
|
// Refresh token if expired
|
|
if token.IsExpired() {
|
|
newToken, err := t.refreshToken(req.Context(), token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
token = newToken
|
|
}
|
|
|
|
// Add Authorization header
|
|
req.Header.Set("Authorization", "Bearer "+token.OAuth2Token.AccessToken)
|
|
req.Header.Set("User-Agent", t.userAgent)
|
|
|
|
// Execute request with retry logic
|
|
var resp *http.Response
|
|
maxRetries := 3
|
|
backoff := 200 * time.Millisecond // Initial backoff duration
|
|
|
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
|
resp, err = t.base.RoundTrip(req)
|
|
if err != nil {
|
|
// Network error, retry with backoff
|
|
time.Sleep(backoff)
|
|
backoff *= 2 // Exponential backoff
|
|
continue
|
|
}
|
|
|
|
// Handle token expiration during request (e.g. token revoked)
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
resp.Body.Close()
|
|
// Refresh token and update request
|
|
token, err = t.refreshToken(req.Context(), token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token.OAuth2Token.AccessToken)
|
|
continue
|
|
}
|
|
|
|
// Retry server errors (5xx) and rate limits (429)
|
|
if resp.StatusCode >= 500 && resp.StatusCode < 600 || resp.StatusCode == http.StatusTooManyRequests {
|
|
resp.Body.Close()
|
|
time.Sleep(backoff)
|
|
backoff *= 2
|
|
continue
|
|
}
|
|
|
|
// Successful response
|
|
return resp, nil
|
|
}
|
|
|
|
// Return last error or response if max retries exceeded
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// refreshToken handles token refresh with mutex protection
|
|
func (t *AuthTransport) refreshToken(ctx context.Context, token *Token) (*Token, error) {
|
|
t.mutex.Lock()
|
|
defer t.mutex.Unlock()
|
|
|
|
// Check again in case another goroutine refreshed while waiting
|
|
currentToken, err := t.storage.GetToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !currentToken.IsExpired() {
|
|
return currentToken, nil
|
|
}
|
|
|
|
// Perform refresh
|
|
newToken, err := t.auth.RefreshToken(ctx, token.OAuth2Token.RefreshToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Save new token
|
|
if err := t.storage.StoreToken(newToken); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newToken, nil
|
|
}
|
|
|
|
// NewDefaultAuthTransport creates a transport with persistent storage
|
|
func NewDefaultAuthTransport(auth *GarthAuthenticator) *AuthTransport {
|
|
return NewAuthTransport(auth, NewFileStorage("garmin_session.json"), nil)
|
|
}
|
|
|
|
// NewMemoryAuthTransport creates a transport with in-memory storage (for testing)
|
|
func NewMemoryAuthTransport(auth *GarthAuthenticator) *AuthTransport {
|
|
return NewAuthTransport(auth, NewMemoryStorage(), nil)
|
|
}
|
|
|
|
// cloneRequest returns a clone of the provided HTTP request
|
|
func cloneRequest(r *http.Request) *http.Request {
|
|
// Shallow copy of the struct
|
|
clone := *r
|
|
// Deep copy of the headers
|
|
clone.Header = make(http.Header, len(r.Header))
|
|
for k, v := range r.Header {
|
|
clone.Header[k] = v
|
|
}
|
|
return &clone
|
|
}
|