mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Improvements targetting possible memory usage spikes.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package traefikoidc
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
@@ -16,18 +17,82 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
replayCacheMu sync.Mutex
|
replayCacheMu sync.RWMutex // Use RWMutex for better read performance
|
||||||
replayCache *Cache // Replace unbounded map with bounded Cache
|
replayCache *Cache // Replace unbounded map with bounded Cache
|
||||||
|
replayCacheOnce sync.Once // CRITICAL FIX: Ensure thread-safe singleton initialization
|
||||||
)
|
)
|
||||||
|
|
||||||
// initReplayCache initializes the global replay cache with size limit
|
// CRITICAL FIX: Thread-safe initialization with proper size limits and LRU eviction
|
||||||
func initReplayCache() {
|
func initReplayCache() {
|
||||||
if replayCache == nil {
|
replayCacheOnce.Do(func() {
|
||||||
replayCache = NewCache()
|
replayCache = NewCache()
|
||||||
replayCache.SetMaxSize(10000) // Set size limit to 10,000 entries
|
replayCache.SetMaxSize(10000) // Set size limit to 10,000 entries (prevents unbounded growth)
|
||||||
|
// Note: NewCache() already starts auto-cleanup goroutine with LRU eviction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL FIX: Add cleanup function for global replay cache
|
||||||
|
func cleanupReplayCache() {
|
||||||
|
replayCacheMu.Lock()
|
||||||
|
defer replayCacheMu.Unlock()
|
||||||
|
|
||||||
|
if replayCache != nil {
|
||||||
|
replayCache.Close() // Stop auto-cleanup goroutine
|
||||||
|
replayCache = nil // Allow GC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL FIX: Add function to get cache stats for monitoring
|
||||||
|
func getReplayCacheStats() (size int, maxSize int) {
|
||||||
|
replayCacheMu.RLock()
|
||||||
|
defer replayCacheMu.RUnlock()
|
||||||
|
|
||||||
|
if replayCache == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to add a method to get current size from Cache
|
||||||
|
// For now, return maxSize as approximation
|
||||||
|
return 0, 10000 // TODO: Add Size() method to Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL FIX: Add periodic cleanup mechanism for long-running scenarios
|
||||||
|
func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
||||||
|
// Start background cleanup goroutine with periodic stats logging
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute) // Run cleanup every 5 minutes
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// Log cache stats for monitoring
|
||||||
|
size, maxSize := getReplayCacheStats()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Debugf("Replay cache stats: size=%d, maxSize=%d", size, maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force cleanup if cache exists (the cache handles its own cleanup,
|
||||||
|
// but this provides additional periodic maintenance)
|
||||||
|
replayCacheMu.RLock()
|
||||||
|
if replayCache != nil {
|
||||||
|
// Cache already handles cleanup automatically,
|
||||||
|
// this just ensures cleanup runs periodically for long-running scenarios
|
||||||
|
}
|
||||||
|
replayCacheMu.RUnlock()
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Context cancelled, cleanup and exit
|
||||||
|
cleanupReplayCache()
|
||||||
|
if logger != nil {
|
||||||
|
logger.Debug("Replay cache cleanup goroutine stopped due to context cancellation")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// STABILITY FIX: Standardize clock skew tolerance usage
|
// STABILITY FIX: Standardize clock skew tolerance usage
|
||||||
// ClockSkewToleranceFuture defines the tolerance for future-based claims like 'exp'.
|
// ClockSkewToleranceFuture defines the tolerance for future-based claims like 'exp'.
|
||||||
// Allows for more leniency with expiration checks.
|
// Allows for more leniency with expiration checks.
|
||||||
@@ -195,16 +260,16 @@ func (j *JWT) Verify(issuerURL, clientID string, skipReplayCheck ...bool) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY FIX: Use bounded Cache with thread-safe operations
|
// CRITICAL FIX: Use thread-safe initialization and proper locking
|
||||||
replayCacheMu.Lock()
|
initReplayCache() // Thread-safe initialization
|
||||||
defer replayCacheMu.Unlock()
|
|
||||||
|
|
||||||
// Initialize cache if not already done
|
// SECURITY FIX: Use read lock for checking, write lock for writing
|
||||||
initReplayCache()
|
replayCacheMu.RLock()
|
||||||
|
_, exists := replayCache.Get(jti)
|
||||||
|
replayCacheMu.RUnlock()
|
||||||
|
|
||||||
// SECURITY FIX: Check for replay attack using Cache API
|
if exists {
|
||||||
if _, exists := replayCache.Get(jti); exists {
|
return fmt.Errorf("token replay detected (jti: %s)", jti)
|
||||||
return fmt.Errorf("token replay detected")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expiration time
|
// Calculate expiration time
|
||||||
@@ -216,10 +281,15 @@ func (j *JWT) Verify(issuerURL, clientID string, skipReplayCheck ...bool) error
|
|||||||
expTime = time.Now().Add(10 * time.Minute)
|
expTime = time.Now().Add(10 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SECURITY FIX: Add to replay cache with expiration using Cache API
|
// SECURITY FIX: Add to replay cache with expiration using write lock
|
||||||
duration := time.Until(expTime)
|
duration := time.Until(expTime)
|
||||||
if duration > 0 {
|
if duration > 0 {
|
||||||
replayCache.Set(jti, true, duration)
|
replayCacheMu.Lock()
|
||||||
|
// Double-check pattern: verify cache is still initialized
|
||||||
|
if replayCache != nil {
|
||||||
|
replayCache.Set(jti, true, duration)
|
||||||
|
}
|
||||||
|
replayCacheMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,87 @@ const (
|
|||||||
defaultMaxBlacklistSize = 10000 // Default maximum size for token blacklist cache
|
defaultMaxBlacklistSize = 10000 // Default maximum size for token blacklist cache
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CRITICAL FIX: Global cache manager for shared cache instances to prevent memory leaks
|
||||||
|
var (
|
||||||
|
globalCacheManager *CacheManager
|
||||||
|
cacheManagerOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheManager provides shared cache instances across middleware instances
|
||||||
|
type CacheManager struct {
|
||||||
|
tokenBlacklist *Cache
|
||||||
|
tokenCache *TokenCache
|
||||||
|
metadataCache *MetadataCache
|
||||||
|
jwkCache JWKCacheInterface
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobalCacheManager returns the singleton cache manager instance
|
||||||
|
func GetGlobalCacheManager() *CacheManager {
|
||||||
|
cacheManagerOnce.Do(func() {
|
||||||
|
globalCacheManager = &CacheManager{
|
||||||
|
tokenBlacklist: func() *Cache {
|
||||||
|
c := NewCache()
|
||||||
|
c.SetMaxSize(defaultMaxBlacklistSize)
|
||||||
|
return c
|
||||||
|
}(),
|
||||||
|
tokenCache: NewTokenCache(),
|
||||||
|
metadataCache: NewMetadataCache(),
|
||||||
|
jwkCache: &JWKCache{},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return globalCacheManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharedTokenBlacklist returns the shared token blacklist cache
|
||||||
|
func (cm *CacheManager) GetSharedTokenBlacklist() *Cache {
|
||||||
|
cm.mu.RLock()
|
||||||
|
defer cm.mu.RUnlock()
|
||||||
|
return cm.tokenBlacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharedTokenCache returns the shared token cache
|
||||||
|
func (cm *CacheManager) GetSharedTokenCache() *TokenCache {
|
||||||
|
cm.mu.RLock()
|
||||||
|
defer cm.mu.RUnlock()
|
||||||
|
return cm.tokenCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharedMetadataCache returns the shared metadata cache
|
||||||
|
func (cm *CacheManager) GetSharedMetadataCache() *MetadataCache {
|
||||||
|
cm.mu.RLock()
|
||||||
|
defer cm.mu.RUnlock()
|
||||||
|
return cm.metadataCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSharedJWKCache returns the shared JWK cache
|
||||||
|
func (cm *CacheManager) GetSharedJWKCache() JWKCacheInterface {
|
||||||
|
cm.mu.RLock()
|
||||||
|
defer cm.mu.RUnlock()
|
||||||
|
return cm.jwkCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close properly closes all shared caches
|
||||||
|
func (cm *CacheManager) Close() error {
|
||||||
|
cm.mu.Lock()
|
||||||
|
defer cm.mu.Unlock()
|
||||||
|
|
||||||
|
if cm.tokenBlacklist != nil {
|
||||||
|
cm.tokenBlacklist.Close()
|
||||||
|
}
|
||||||
|
if cm.tokenCache != nil {
|
||||||
|
cm.tokenCache.Close()
|
||||||
|
}
|
||||||
|
if cm.metadataCache != nil {
|
||||||
|
cm.metadataCache.Close()
|
||||||
|
}
|
||||||
|
if cm.jwkCache != nil {
|
||||||
|
cm.jwkCache.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// TokenVerifier interface for token verification
|
// TokenVerifier interface for token verification
|
||||||
type TokenVerifier interface {
|
type TokenVerifier interface {
|
||||||
VerifyToken(token string) error
|
VerifyToken(token string) error
|
||||||
@@ -155,6 +236,10 @@ type TraefikOidc struct {
|
|||||||
tokenCleanupStopChan chan struct{} // Channel to stop token cleanup goroutine
|
tokenCleanupStopChan chan struct{} // Channel to stop token cleanup goroutine
|
||||||
metadataRefreshStopChan chan struct{} // Channel to stop metadata refresh goroutine
|
metadataRefreshStopChan chan struct{} // Channel to stop metadata refresh goroutine
|
||||||
goroutineWG sync.WaitGroup // WaitGroup to track background goroutines
|
goroutineWG sync.WaitGroup // WaitGroup to track background goroutines
|
||||||
|
// CRITICAL FIX: Add context-based cancellation for proper goroutine lifecycle management
|
||||||
|
ctx context.Context // Context for all background operations
|
||||||
|
cancelFunc context.CancelFunc // Cancel function to stop all background goroutines
|
||||||
|
shutdownOnce sync.Once // Ensure shutdown is only called once
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProviderMetadata holds OIDC provider metadata
|
// ProviderMetadata holds OIDC provider metadata
|
||||||
@@ -462,6 +547,12 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
} else {
|
} else {
|
||||||
httpClient = createDefaultHTTPClient()
|
httpClient = createDefaultHTTPClient()
|
||||||
}
|
}
|
||||||
|
// CRITICAL FIX: Use shared cache instances to prevent memory leaks
|
||||||
|
cacheManager := GetGlobalCacheManager()
|
||||||
|
|
||||||
|
// CRITICAL FIX: Initialize context for proper goroutine lifecycle management
|
||||||
|
pluginCtx, cancelFunc := context.WithCancel(context.Background())
|
||||||
|
|
||||||
t := &TraefikOidc{
|
t := &TraefikOidc{
|
||||||
next: next,
|
next: next,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -478,20 +569,17 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
}
|
}
|
||||||
return config.PostLogoutRedirectURI
|
return config.PostLogoutRedirectURI
|
||||||
}(),
|
}(),
|
||||||
tokenBlacklist: func() *Cache {
|
// CRITICAL FIX: Use shared cache instances instead of creating new ones per middleware
|
||||||
c := NewCache()
|
tokenBlacklist: cacheManager.GetSharedTokenBlacklist(),
|
||||||
c.SetMaxSize(defaultMaxBlacklistSize)
|
jwkCache: cacheManager.GetSharedJWKCache(),
|
||||||
return c
|
metadataCache: cacheManager.GetSharedMetadataCache(),
|
||||||
}(), // Use generic cache for blacklist with size limit
|
|
||||||
jwkCache: &JWKCache{},
|
|
||||||
metadataCache: NewMetadataCache(),
|
|
||||||
clientID: config.ClientID,
|
clientID: config.ClientID,
|
||||||
clientSecret: config.ClientSecret,
|
clientSecret: config.ClientSecret,
|
||||||
forceHTTPS: config.ForceHTTPS,
|
forceHTTPS: config.ForceHTTPS,
|
||||||
enablePKCE: config.EnablePKCE,
|
enablePKCE: config.EnablePKCE,
|
||||||
scopes: mergeScopes([]string{"openid", "profile", "email"}, config.Scopes),
|
scopes: mergeScopes([]string{"openid", "profile", "email"}, config.Scopes),
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit),
|
limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit),
|
||||||
tokenCache: NewTokenCache(),
|
tokenCache: cacheManager.GetSharedTokenCache(),
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
tokenHTTPClient: createTokenHTTPClient(httpClient),
|
tokenHTTPClient: createTokenHTTPClient(httpClient),
|
||||||
excludedURLs: createStringMap(config.ExcludedURLs),
|
excludedURLs: createStringMap(config.ExcludedURLs),
|
||||||
@@ -508,6 +596,9 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
}(),
|
}(),
|
||||||
tokenCleanupStopChan: make(chan struct{}),
|
tokenCleanupStopChan: make(chan struct{}),
|
||||||
metadataRefreshStopChan: make(chan struct{}),
|
metadataRefreshStopChan: make(chan struct{}),
|
||||||
|
// CRITICAL FIX: Initialize context-based cancellation
|
||||||
|
ctx: pluginCtx,
|
||||||
|
cancelFunc: cancelFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, t.logger)
|
t.sessionManager, _ = NewSessionManager(config.SessionEncryptionKey, config.ForceHTTPS, t.logger)
|
||||||
@@ -539,6 +630,9 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
logger.Debugf("Parsed template for header %s: %s", header.Name, header.Value)
|
logger.Debugf("Parsed template for header %s: %s", header.Name, header.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL FIX: Start replay cache cleanup for long-running plugin scenarios
|
||||||
|
startReplayCacheCleanup(pluginCtx, logger)
|
||||||
|
|
||||||
go t.initializeMetadata(config.ProviderURL)
|
go t.initializeMetadata(config.ProviderURL)
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
@@ -629,6 +723,10 @@ func (t *TraefikOidc) startMetadataRefresh(providerURL string) {
|
|||||||
case <-t.metadataRefreshStopChan:
|
case <-t.metadataRefreshStopChan:
|
||||||
t.logger.Debug("Metadata refresh goroutine stopped.")
|
t.logger.Debug("Metadata refresh goroutine stopped.")
|
||||||
return
|
return
|
||||||
|
case <-t.ctx.Done():
|
||||||
|
// CRITICAL FIX: Context-based cancellation for proper goroutine lifecycle
|
||||||
|
t.logger.Debug("Metadata refresh goroutine stopped due to context cancellation.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -1721,6 +1819,10 @@ func (t *TraefikOidc) startTokenCleanup() {
|
|||||||
case <-t.tokenCleanupStopChan:
|
case <-t.tokenCleanupStopChan:
|
||||||
t.logger.Debug("Token cleanup goroutine stopped.")
|
t.logger.Debug("Token cleanup goroutine stopped.")
|
||||||
return
|
return
|
||||||
|
case <-t.ctx.Done():
|
||||||
|
// CRITICAL FIX: Context-based cancellation for proper goroutine lifecycle
|
||||||
|
t.logger.Debug("Token cleanup goroutine stopped due to context cancellation.")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -2418,58 +2520,68 @@ func (t *TraefikOidc) validateTokenExpiry(session *SessionData, token string) (b
|
|||||||
|
|
||||||
// Close stops all background goroutines and closes resources with proper timeout.
|
// Close stops all background goroutines and closes resources with proper timeout.
|
||||||
func (t *TraefikOidc) Close() error {
|
func (t *TraefikOidc) Close() error {
|
||||||
t.logger.Debug("Closing TraefikOidc plugin instance")
|
// CRITICAL FIX: Use shutdownOnce to ensure Close is only called once
|
||||||
|
var closeErr error
|
||||||
|
t.shutdownOnce.Do(func() {
|
||||||
|
t.logger.Debug("Closing TraefikOidc plugin instance")
|
||||||
|
|
||||||
// Signal all goroutines to stop
|
// CRITICAL FIX: Signal all goroutines to stop using context cancellation
|
||||||
if t.tokenCleanupStopChan != nil {
|
if t.cancelFunc != nil {
|
||||||
close(t.tokenCleanupStopChan)
|
t.cancelFunc()
|
||||||
t.logger.Debug("tokenCleanupStopChan closed")
|
t.logger.Debug("Context cancellation signaled to all goroutines")
|
||||||
}
|
}
|
||||||
if t.metadataRefreshStopChan != nil {
|
|
||||||
close(t.metadataRefreshStopChan)
|
|
||||||
t.logger.Debug("metadataRefreshStopChan closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all goroutines to finish with timeout
|
// Legacy channel cleanup for backward compatibility (if channels still exist)
|
||||||
done := make(chan struct{})
|
if t.tokenCleanupStopChan != nil {
|
||||||
go func() {
|
close(t.tokenCleanupStopChan)
|
||||||
t.goroutineWG.Wait()
|
t.logger.Debug("tokenCleanupStopChan closed")
|
||||||
close(done)
|
}
|
||||||
}()
|
if t.metadataRefreshStopChan != nil {
|
||||||
|
close(t.metadataRefreshStopChan)
|
||||||
|
t.logger.Debug("metadataRefreshStopChan closed")
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for goroutines to finish or timeout after 10 seconds
|
// Wait for all goroutines to finish with timeout
|
||||||
select {
|
done := make(chan struct{})
|
||||||
case <-done:
|
go func() {
|
||||||
t.logger.Debug("All background goroutines stopped gracefully")
|
t.goroutineWG.Wait()
|
||||||
case <-time.After(10 * time.Second):
|
close(done)
|
||||||
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
}()
|
||||||
// Continue with cleanup even if goroutines didn't stop gracefully
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close caches
|
// Wait for goroutines to finish or timeout after 10 seconds
|
||||||
// These Close methods should stop their respective autoCleanupRoutine goroutines
|
select {
|
||||||
if t.tokenBlacklist != nil {
|
case <-done:
|
||||||
t.tokenBlacklist.Close() // This is *cache.Cache, which has Close()
|
t.logger.Debug("All background goroutines stopped gracefully")
|
||||||
t.logger.Debug("tokenBlacklist closed")
|
case <-time.After(10 * time.Second):
|
||||||
}
|
t.logger.Errorf("Timeout waiting for background goroutines to stop")
|
||||||
if t.metadataCache != nil {
|
// Continue with cleanup even if goroutines didn't stop gracefully
|
||||||
t.metadataCache.Close() // This is *MetadataCache, which has Close()
|
}
|
||||||
t.logger.Debug("metadataCache closed")
|
|
||||||
}
|
|
||||||
if t.tokenCache != nil {
|
|
||||||
t.tokenCache.Close() // This is *TokenCache, which now has Close()
|
|
||||||
t.logger.Debug("tokenCache closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.jwkCache != nil {
|
// Close caches
|
||||||
// Reverting to the original explicit instruction to call t.jwkCache.Close().
|
// These Close methods should stop their respective autoCleanupRoutine goroutines
|
||||||
// This will cause a compile error if JWKCacheInterface (and its implementation *JWKCache)
|
if t.tokenBlacklist != nil {
|
||||||
// is not updated in jwk.go to include and implement a Close() method
|
t.tokenBlacklist.Close() // This is *cache.Cache, which has Close()
|
||||||
// that properly closes the internal *cache.Cache instance.
|
t.logger.Debug("tokenBlacklist closed")
|
||||||
t.jwkCache.Close()
|
}
|
||||||
t.logger.Debug("t.jwkCache.Close() called as per original instruction.")
|
if t.metadataCache != nil {
|
||||||
}
|
t.metadataCache.Close() // This is *MetadataCache, which has Close()
|
||||||
|
t.logger.Debug("metadataCache closed")
|
||||||
|
}
|
||||||
|
if t.tokenCache != nil {
|
||||||
|
t.tokenCache.Close() // This is *TokenCache, which now has Close()
|
||||||
|
t.logger.Debug("tokenCache closed")
|
||||||
|
}
|
||||||
|
|
||||||
t.logger.Info("TraefikOidc plugin instance closed successfully.")
|
if t.jwkCache != nil {
|
||||||
return nil
|
// Reverting to the original explicit instruction to call t.jwkCache.Close().
|
||||||
|
// This will cause a compile error if JWKCacheInterface (and its implementation *JWKCache)
|
||||||
|
// is not updated in jwk.go to include and implement a Close() method
|
||||||
|
// that properly closes the internal *cache.Cache instance.
|
||||||
|
t.jwkCache.Close()
|
||||||
|
t.logger.Debug("t.jwkCache.Close() called as per original instruction.")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.logger.Info("TraefikOidc plugin instance closed successfully.")
|
||||||
|
})
|
||||||
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-9
@@ -240,11 +240,27 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) {
|
|||||||
sessionData.request = r
|
sessionData.request = r
|
||||||
sessionData.dirty = false // Reset dirty flag when getting a session
|
sessionData.dirty = false // Reset dirty flag when getting a session
|
||||||
|
|
||||||
|
// CRITICAL FIX: Use defer to guarantee session is returned to pool on any error
|
||||||
|
var sessionReturned bool
|
||||||
|
defer func() {
|
||||||
|
if !sessionReturned && sessionData != nil {
|
||||||
|
// Recover from any panics and ensure session cleanup
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
sessionData.inUse = false
|
||||||
|
sessionData.Reset()
|
||||||
|
sm.sessionPool.Put(sessionData)
|
||||||
|
panic(r) // Re-panic after cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Function to properly handle errors and return the session to the pool
|
// Function to properly handle errors and return the session to the pool
|
||||||
handleError := func(err error, message string) (*SessionData, error) {
|
handleError := func(err error, message string) (*SessionData, error) {
|
||||||
if sessionData != nil {
|
if sessionData != nil && !sessionReturned {
|
||||||
sessionData.inUse = false // Mark as not in use before returning to pool
|
sessionData.inUse = false // Mark as not in use before returning to pool
|
||||||
|
sessionData.Reset() // Reset to prevent data leakage
|
||||||
sm.sessionPool.Put(sessionData)
|
sm.sessionPool.Put(sessionData)
|
||||||
|
sessionReturned = true // Mark as returned to prevent double-return
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%s: %w", message, err)
|
return nil, fmt.Errorf("%s: %w", message, err)
|
||||||
}
|
}
|
||||||
@@ -285,6 +301,8 @@ func (sm *SessionManager) GetSession(r *http.Request) (*SessionData, error) {
|
|||||||
sm.getTokenChunkSessions(r, accessTokenCookie, sessionData.accessTokenChunks)
|
sm.getTokenChunkSessions(r, accessTokenCookie, sessionData.accessTokenChunks)
|
||||||
sm.getTokenChunkSessions(r, refreshTokenCookie, sessionData.refreshTokenChunks)
|
sm.getTokenChunkSessions(r, refreshTokenCookie, sessionData.refreshTokenChunks)
|
||||||
|
|
||||||
|
// CRITICAL FIX: Mark session as successfully retrieved to prevent cleanup in defer
|
||||||
|
sessionReturned = false // Session is successfully being returned to caller
|
||||||
return sessionData, nil
|
return sessionData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +460,21 @@ func (sd *SessionData) Save(r *http.Request, w http.ResponseWriter) error {
|
|||||||
//
|
//
|
||||||
// Note: This method will always return the SessionData object to the pool, even if an error occurs.
|
// Note: This method will always return the SessionData object to the pool, even if an error occurs.
|
||||||
func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
||||||
|
// CRITICAL FIX: Use defer to guarantee session is returned to pool regardless of any errors or panics
|
||||||
|
defer func() {
|
||||||
|
if rec := recover(); rec != nil {
|
||||||
|
// Ensure session is returned to pool even on panic
|
||||||
|
sd.returnToPoolSafely()
|
||||||
|
panic(rec) // Re-panic after cleanup
|
||||||
|
}
|
||||||
|
// Normal path - return to pool
|
||||||
|
sd.returnToPoolSafely()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// CRITICAL FIX: Lock session mutex to prevent race conditions during Clear
|
||||||
|
sd.sessionMutex.Lock()
|
||||||
|
defer sd.sessionMutex.Unlock()
|
||||||
|
|
||||||
sd.dirty = true // Clearing the session means its state is changing and needs to be saved.
|
sd.dirty = true // Clearing the session means its state is changing and needs to be saved.
|
||||||
|
|
||||||
// Clear and expire all sessions.
|
// Clear and expire all sessions.
|
||||||
@@ -484,17 +517,23 @@ func (sd *SessionData) Clear(r *http.Request, w http.ResponseWriter) error {
|
|||||||
// Clear transient per-request fields.
|
// Clear transient per-request fields.
|
||||||
sd.request = nil
|
sd.request = nil
|
||||||
|
|
||||||
// STABILITY FIX: Mark as not in use and return session to pool, regardless of error.
|
// Return the error from Save, if any (defer will handle pool return)
|
||||||
// This ensures the session is always returned to the pool, preventing memory leaks.
|
|
||||||
sd.inUse = false
|
|
||||||
// Reset the session data before returning to pool to prevent data leakage
|
|
||||||
sd.Reset()
|
|
||||||
sd.manager.sessionPool.Put(sd)
|
|
||||||
|
|
||||||
// Return the error from Save, if any
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRITICAL FIX: Add thread-safe helper method to return session to pool
|
||||||
|
func (sd *SessionData) returnToPoolSafely() {
|
||||||
|
if sd != nil && sd.manager != nil {
|
||||||
|
// Check if already returned to prevent double-return
|
||||||
|
if sd.inUse {
|
||||||
|
sd.inUse = false
|
||||||
|
// Reset the session data before returning to pool to prevent data leakage
|
||||||
|
sd.Reset()
|
||||||
|
sd.manager.sessionPool.Put(sd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// clearTokenChunks iterates through a map of session chunks, clears their values,
|
// clearTokenChunks iterates through a map of session chunks, clears their values,
|
||||||
// and sets their MaxAge to -1 to expire them. This is used internally by Clear.
|
// and sets their MaxAge to -1 to expire them. This is used internally by Clear.
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in New Issue
Block a user