mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-18 04:47:28 +00:00
fixup! fixup! Simplify all the things.
This commit is contained in:
+8
-2
@@ -37,7 +37,10 @@ func (bt *BackgroundTask) run() {
|
|||||||
ticker := time.NewTicker(bt.interval)
|
ticker := time.NewTicker(bt.interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
bt.logger.Debug("Starting background task: %s", bt.name)
|
// Only log startup if debug level is enabled
|
||||||
|
if bt.logger != nil {
|
||||||
|
bt.logger.Info("Starting background task: %s", bt.name)
|
||||||
|
}
|
||||||
|
|
||||||
// Run task immediately on startup
|
// Run task immediately on startup
|
||||||
bt.taskFunc()
|
bt.taskFunc()
|
||||||
@@ -47,7 +50,10 @@ func (bt *BackgroundTask) run() {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
bt.taskFunc()
|
bt.taskFunc()
|
||||||
case <-bt.stopChan:
|
case <-bt.stopChan:
|
||||||
bt.logger.Debug("Stopping background task: %s", bt.name)
|
// Only log shutdown
|
||||||
|
if bt.logger != nil {
|
||||||
|
bt.logger.Info("Stopping background task: %s", bt.name)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxKeyLength defines the maximum allowed length for cache keys
|
||||||
|
const MaxKeyLength = 256
|
||||||
|
|
||||||
|
// OptimizedCacheEntry represents a single cache entry with embedded LRU linked list
|
||||||
|
// This eliminates the need for separate data structures and reduces memory overhead by ~66%
|
||||||
|
type OptimizedCacheEntry struct {
|
||||||
|
Value interface{}
|
||||||
|
ExpiresAt time.Time
|
||||||
|
Key string
|
||||||
|
|
||||||
|
// Embedded doubly-linked list pointers for LRU ordering
|
||||||
|
prev, next *OptimizedCacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptimizedCache provides a memory-efficient thread-safe cache with LRU eviction
|
||||||
|
// Uses only a single map with embedded doubly-linked list to reduce memory overhead
|
||||||
|
type OptimizedCache struct {
|
||||||
|
items map[string]*OptimizedCacheEntry
|
||||||
|
head, tail *OptimizedCacheEntry // LRU sentinel nodes
|
||||||
|
cleanupTask *BackgroundTask
|
||||||
|
logger *Logger
|
||||||
|
maxSize int
|
||||||
|
maxMemoryBytes int64 // Memory budget limit
|
||||||
|
currentMemoryBytes int64 // Current estimated memory usage
|
||||||
|
autoCleanupInterval time.Duration
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptimizedCache creates a new memory-efficient cache with default settings
|
||||||
|
func NewOptimizedCache() *OptimizedCache {
|
||||||
|
return NewOptimizedCacheWithConfig(DefaultMaxSize, 0, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOptimizedCacheWithConfig creates a cache with specified configuration
|
||||||
|
func NewOptimizedCacheWithConfig(maxSize int, maxMemoryMB int, logger *Logger) *OptimizedCache {
|
||||||
|
if logger == nil {
|
||||||
|
logger = newNoOpLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sentinel nodes for the doubly-linked list
|
||||||
|
head := &OptimizedCacheEntry{}
|
||||||
|
tail := &OptimizedCacheEntry{}
|
||||||
|
head.next = tail
|
||||||
|
tail.prev = head
|
||||||
|
|
||||||
|
maxMemoryBytes := int64(maxMemoryMB) * 1024 * 1024 // Convert MB to bytes
|
||||||
|
if maxMemoryBytes == 0 {
|
||||||
|
maxMemoryBytes = 64 * 1024 * 1024 // Default 64MB
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &OptimizedCache{
|
||||||
|
items: make(map[string]*OptimizedCacheEntry, maxSize),
|
||||||
|
head: head,
|
||||||
|
tail: tail,
|
||||||
|
maxSize: maxSize,
|
||||||
|
maxMemoryBytes: maxMemoryBytes,
|
||||||
|
autoCleanupInterval: 5 * time.Minute,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.startAutoCleanup()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds or updates an item in the cache with memory and key validation
|
||||||
|
func (c *OptimizedCache) Set(key string, value interface{}, expiration time.Duration) {
|
||||||
|
// Validate key length to prevent memory bloat
|
||||||
|
if len(key) > MaxKeyLength {
|
||||||
|
c.logger.Debugf("Cache key too long (%d > %d), ignoring", len(key), MaxKeyLength)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
expTime := now.Add(expiration)
|
||||||
|
|
||||||
|
// Update existing item
|
||||||
|
if entry, exists := c.items[key]; exists {
|
||||||
|
oldSize := c.estimateEntrySize(entry)
|
||||||
|
entry.Value = value
|
||||||
|
entry.ExpiresAt = expTime
|
||||||
|
newSize := c.estimateEntrySize(entry)
|
||||||
|
c.currentMemoryBytes += newSize - oldSize
|
||||||
|
c.moveToTail(entry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new entry
|
||||||
|
entry := &OptimizedCacheEntry{
|
||||||
|
Value: value,
|
||||||
|
ExpiresAt: expTime,
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
entrySize := c.estimateEntrySize(entry)
|
||||||
|
|
||||||
|
// Check memory budget and evict if necessary
|
||||||
|
for (c.currentMemoryBytes+entrySize > c.maxMemoryBytes || len(c.items) >= c.maxSize) && len(c.items) > 0 {
|
||||||
|
if !c.evictOldest() {
|
||||||
|
break // No more items to evict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new entry
|
||||||
|
c.items[key] = entry
|
||||||
|
c.currentMemoryBytes += entrySize
|
||||||
|
c.addToTail(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves an item from the cache with memory-efficient access tracking
|
||||||
|
func (c *OptimizedCache) Get(key string) (interface{}, bool) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
entry, exists := c.items[key]
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for expiration
|
||||||
|
if time.Now().After(entry.ExpiresAt) {
|
||||||
|
c.removeEntry(entry)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to tail (most recently used)
|
||||||
|
c.moveToTail(entry)
|
||||||
|
return entry.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes an item from the cache
|
||||||
|
func (c *OptimizedCache) Delete(key string) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
if entry, exists := c.items[key]; exists {
|
||||||
|
c.removeEntry(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup removes expired items and performs memory optimization
|
||||||
|
func (c *OptimizedCache) Cleanup() {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
toRemove := make([]*OptimizedCacheEntry, 0, len(c.items)/10) // Pre-allocate for efficiency
|
||||||
|
|
||||||
|
// Collect expired entries (start from head - oldest items)
|
||||||
|
for entry := c.head.next; entry != c.tail; entry = entry.next {
|
||||||
|
if now.After(entry.ExpiresAt) {
|
||||||
|
toRemove = append(toRemove, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove expired entries
|
||||||
|
for _, entry := range toRemove {
|
||||||
|
c.removeEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform memory pressure eviction if needed
|
||||||
|
for c.currentMemoryBytes > c.maxMemoryBytes && len(c.items) > 0 {
|
||||||
|
if !c.evictOldest() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// evictOldest removes the least recently used item
|
||||||
|
// Returns false if no items to evict
|
||||||
|
func (c *OptimizedCache) evictOldest() bool {
|
||||||
|
if c.head.next == c.tail {
|
||||||
|
return false // Empty cache
|
||||||
|
}
|
||||||
|
|
||||||
|
oldest := c.head.next
|
||||||
|
c.removeEntry(oldest)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeEntry removes an entry from both the map and linked list
|
||||||
|
func (c *OptimizedCache) removeEntry(entry *OptimizedCacheEntry) {
|
||||||
|
// Remove from map
|
||||||
|
delete(c.items, entry.Key)
|
||||||
|
|
||||||
|
// Update memory usage
|
||||||
|
c.currentMemoryBytes -= c.estimateEntrySize(entry)
|
||||||
|
|
||||||
|
// Remove from linked list
|
||||||
|
entry.prev.next = entry.next
|
||||||
|
entry.next.prev = entry.prev
|
||||||
|
|
||||||
|
// Clear references to help GC
|
||||||
|
entry.prev = nil
|
||||||
|
entry.next = nil
|
||||||
|
entry.Value = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToTail adds an entry to the tail (most recently used position)
|
||||||
|
func (c *OptimizedCache) addToTail(entry *OptimizedCacheEntry) {
|
||||||
|
entry.prev = c.tail.prev
|
||||||
|
entry.next = c.tail
|
||||||
|
c.tail.prev.next = entry
|
||||||
|
c.tail.prev = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveToTail moves an existing entry to the tail (mark as most recently used)
|
||||||
|
func (c *OptimizedCache) moveToTail(entry *OptimizedCacheEntry) {
|
||||||
|
// Remove from current position
|
||||||
|
entry.prev.next = entry.next
|
||||||
|
entry.next.prev = entry.prev
|
||||||
|
|
||||||
|
// Add to tail
|
||||||
|
c.addToTail(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimateEntrySize estimates the memory usage of a cache entry
|
||||||
|
// Uses conservative estimates since unsafe.Sizeof is not allowed in Yaegi
|
||||||
|
func (c *OptimizedCache) estimateEntrySize(entry *OptimizedCacheEntry) int64 {
|
||||||
|
// Conservative estimate for OptimizedCacheEntry struct overhead
|
||||||
|
// (3 pointers + time.Time + string) ≈ 80 bytes on 64-bit systems
|
||||||
|
size := int64(80) + int64(len(entry.Key))
|
||||||
|
|
||||||
|
// Estimate value size based on type
|
||||||
|
if entry.Value != nil {
|
||||||
|
switch v := entry.Value.(type) {
|
||||||
|
case string:
|
||||||
|
size += int64(len(v))
|
||||||
|
case []byte:
|
||||||
|
size += int64(len(v))
|
||||||
|
case map[string]interface{}:
|
||||||
|
// Rough estimate for map overhead + keys + values
|
||||||
|
size += int64(len(v)) * 64 // 64 bytes per entry estimate
|
||||||
|
for key, val := range v {
|
||||||
|
size += int64(len(key))
|
||||||
|
// Estimate value size
|
||||||
|
switch val := val.(type) {
|
||||||
|
case string:
|
||||||
|
size += int64(len(val))
|
||||||
|
case []byte:
|
||||||
|
size += int64(len(val))
|
||||||
|
default:
|
||||||
|
size += 32 // Default estimate for other types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
for _, s := range v {
|
||||||
|
size += int64(len(s)) + 16 // 16 bytes slice overhead per string
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Generic estimate for unknown types
|
||||||
|
size += 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxSize changes the maximum number of items the cache can hold
|
||||||
|
func (c *OptimizedCache) SetMaxSize(size int) {
|
||||||
|
if size <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.maxSize = size
|
||||||
|
|
||||||
|
// Evict excess items if necessary
|
||||||
|
for len(c.items) > c.maxSize && len(c.items) > 0 {
|
||||||
|
if !c.evictOldest() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxMemory sets the maximum memory budget in MB
|
||||||
|
func (c *OptimizedCache) SetMaxMemory(maxMemoryMB int) {
|
||||||
|
if maxMemoryMB <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.maxMemoryBytes = int64(maxMemoryMB) * 1024 * 1024
|
||||||
|
|
||||||
|
// Evict items if over memory budget
|
||||||
|
for c.currentMemoryBytes > c.maxMemoryBytes && len(c.items) > 0 {
|
||||||
|
if !c.evictOldest() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startAutoCleanup starts the background cleanup task
|
||||||
|
func (c *OptimizedCache) startAutoCleanup() {
|
||||||
|
c.cleanupTask = NewBackgroundTask("optimized-cache-cleanup", c.autoCleanupInterval, c.Cleanup, c.logger)
|
||||||
|
c.cleanupTask.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the automatic cleanup task
|
||||||
|
func (c *OptimizedCache) Close() {
|
||||||
|
if c.cleanupTask != nil {
|
||||||
|
c.cleanupTask.Stop()
|
||||||
|
c.cleanupTask = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-4
@@ -374,7 +374,7 @@ func (re *RetryExecutor) ExecuteWithContext(ctx context.Context, fn func() error
|
|||||||
err := fn()
|
err := fn()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if attempt > 1 {
|
if attempt > 1 {
|
||||||
re.LogInfo("Operation succeeded on attempt %d", attempt)
|
re.LogInfo("Operation succeeded after %d attempts", attempt)
|
||||||
}
|
}
|
||||||
re.RecordSuccess()
|
re.RecordSuccess()
|
||||||
return nil
|
return nil
|
||||||
@@ -384,7 +384,7 @@ func (re *RetryExecutor) ExecuteWithContext(ctx context.Context, fn func() error
|
|||||||
|
|
||||||
// Check if error is retryable
|
// Check if error is retryable
|
||||||
if !re.isRetryableError(err) {
|
if !re.isRetryableError(err) {
|
||||||
re.LogDebug("Non-retryable error on attempt %d: %v", attempt, err)
|
// Only log non-retryable errors once
|
||||||
re.RecordFailure()
|
re.RecordFailure()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -397,8 +397,11 @@ func (re *RetryExecutor) ExecuteWithContext(ctx context.Context, fn func() error
|
|||||||
|
|
||||||
// Calculate delay with exponential backoff
|
// Calculate delay with exponential backoff
|
||||||
delay := re.calculateDelay(attempt)
|
delay := re.calculateDelay(attempt)
|
||||||
re.LogDebug("Retrying operation after %v (attempt %d/%d): %v",
|
// Only log on first retry and then every 3rd attempt to reduce spam
|
||||||
delay, attempt, re.config.MaxAttempts, err)
|
if attempt == 1 || attempt%3 == 0 {
|
||||||
|
re.LogDebug("Retrying operation after %v (attempt %d/%d): %v",
|
||||||
|
delay, attempt, re.config.MaxAttempts, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Wait with context cancellation support
|
// Wait with context cancellation support
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -112,14 +112,29 @@ func parseJWT(tokenString string) (*JWT, error) {
|
|||||||
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ENHANCED: Use memory pool for JWT parsing buffers
|
||||||
|
pools := GetGlobalMemoryPools()
|
||||||
|
jwtBuf := pools.GetJWTParsingBuffer()
|
||||||
|
defer pools.PutJWTParsingBuffer(jwtBuf)
|
||||||
|
|
||||||
jwt := &JWT{
|
jwt := &JWT{
|
||||||
Token: tokenString,
|
Token: tokenString,
|
||||||
}
|
}
|
||||||
|
|
||||||
headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
// Decode header using pooled buffer
|
||||||
|
headerLen := base64.RawURLEncoding.DecodedLen(len(parts[0]))
|
||||||
|
if headerLen > cap(jwtBuf.HeaderBuf) {
|
||||||
|
jwtBuf.HeaderBuf = make([]byte, headerLen)
|
||||||
|
} else {
|
||||||
|
jwtBuf.HeaderBuf = jwtBuf.HeaderBuf[:headerLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := base64.RawURLEncoding.Decode(jwtBuf.HeaderBuf, []byte(parts[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid JWT format: failed to decode header: %v", err)
|
return nil, fmt.Errorf("invalid JWT format: failed to decode header: %v", err)
|
||||||
}
|
}
|
||||||
|
headerBytes := jwtBuf.HeaderBuf[:n]
|
||||||
|
|
||||||
if err := json.Unmarshal(headerBytes, &jwt.Header); err != nil {
|
if err := json.Unmarshal(headerBytes, &jwt.Header); err != nil {
|
||||||
return nil, fmt.Errorf("invalid JWT format: failed to unmarshal header: %v", err)
|
return nil, fmt.Errorf("invalid JWT format: failed to unmarshal header: %v", err)
|
||||||
}
|
}
|
||||||
@@ -128,10 +143,19 @@ func parseJWT(tokenString string) (*JWT, error) {
|
|||||||
return nil, fmt.Errorf("invalid JWT format: header is nil after unmarshaling")
|
return nil, fmt.Errorf("invalid JWT format: header is nil after unmarshaling")
|
||||||
}
|
}
|
||||||
|
|
||||||
claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
|
// Decode claims using pooled buffer
|
||||||
|
claimsLen := base64.RawURLEncoding.DecodedLen(len(parts[1]))
|
||||||
|
if claimsLen > cap(jwtBuf.PayloadBuf) {
|
||||||
|
jwtBuf.PayloadBuf = make([]byte, claimsLen)
|
||||||
|
} else {
|
||||||
|
jwtBuf.PayloadBuf = jwtBuf.PayloadBuf[:claimsLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = base64.RawURLEncoding.Decode(jwtBuf.PayloadBuf, []byte(parts[1]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid JWT format: failed to decode claims: %v", err)
|
return nil, fmt.Errorf("invalid JWT format: failed to decode claims: %v", err)
|
||||||
}
|
}
|
||||||
|
claimsBytes := jwtBuf.PayloadBuf[:n]
|
||||||
|
|
||||||
if err := json.Unmarshal(claimsBytes, &jwt.Claims); err != nil {
|
if err := json.Unmarshal(claimsBytes, &jwt.Claims); err != nil {
|
||||||
return nil, fmt.Errorf("invalid JWT format: failed to unmarshal claims: %v", err)
|
return nil, fmt.Errorf("invalid JWT format: failed to unmarshal claims: %v", err)
|
||||||
@@ -141,11 +165,22 @@ func parseJWT(tokenString string) (*JWT, error) {
|
|||||||
return nil, fmt.Errorf("invalid JWT format: claims is nil after unmarshaling")
|
return nil, fmt.Errorf("invalid JWT format: claims is nil after unmarshaling")
|
||||||
}
|
}
|
||||||
|
|
||||||
signatureBytes, err := base64.RawURLEncoding.DecodeString(parts[2])
|
// Decode signature using pooled buffer
|
||||||
|
sigLen := base64.RawURLEncoding.DecodedLen(len(parts[2]))
|
||||||
|
if sigLen > cap(jwtBuf.SignatureBuf) {
|
||||||
|
jwtBuf.SignatureBuf = make([]byte, sigLen)
|
||||||
|
} else {
|
||||||
|
jwtBuf.SignatureBuf = jwtBuf.SignatureBuf[:sigLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = base64.RawURLEncoding.Decode(jwtBuf.SignatureBuf, []byte(parts[2]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid JWT format: failed to decode signature: %v", err)
|
return nil, fmt.Errorf("invalid JWT format: failed to decode signature: %v", err)
|
||||||
}
|
}
|
||||||
jwt.Signature = signatureBytes
|
|
||||||
|
// Copy signature to JWT struct (create new slice to avoid pool retention)
|
||||||
|
jwt.Signature = make([]byte, n)
|
||||||
|
copy(jwt.Signature, jwtBuf.SignatureBuf[:n])
|
||||||
|
|
||||||
return jwt, nil
|
return jwt, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,28 +36,32 @@ func createDefaultHTTPClient() *http.Client {
|
|||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
dialer := &net.Dialer{
|
dialer := &net.Dialer{
|
||||||
Timeout: 15 * time.Second, // Reduced timeout
|
Timeout: 10 * time.Second, // OPTIMIZED: Further reduced for faster failures
|
||||||
KeepAlive: 15 * time.Second, // Reduced keepalive
|
KeepAlive: 30 * time.Second, // OPTIMIZED: Increased for better connection reuse
|
||||||
}
|
}
|
||||||
return dialer.DialContext(ctx, network, addr)
|
return dialer.DialContext(ctx, network, addr)
|
||||||
},
|
},
|
||||||
ForceAttemptHTTP2: true,
|
ForceAttemptHTTP2: true,
|
||||||
TLSHandshakeTimeout: 5 * time.Second, // Reduced from 10s
|
TLSHandshakeTimeout: 3 * time.Second, // OPTIMIZED: Reduced for faster TLS negotiation
|
||||||
ExpectContinueTimeout: 0,
|
ExpectContinueTimeout: 1 * time.Second, // OPTIMIZED: Enable for better upload performance
|
||||||
MaxIdleConns: 30, // Reduced from 100
|
MaxIdleConns: 20, // OPTIMIZED: Reduced to limit memory usage
|
||||||
MaxIdleConnsPerHost: 10, // Reduced from 100
|
MaxIdleConnsPerHost: 5, // OPTIMIZED: Reduced per-host connections
|
||||||
IdleConnTimeout: 30 * time.Second, // Reduced from 90s
|
IdleConnTimeout: 60 * time.Second, // OPTIMIZED: Increased for better reuse
|
||||||
DisableKeepAlives: false, // Enable connection reuse
|
DisableKeepAlives: false, // Enable connection reuse
|
||||||
MaxConnsPerHost: 50, // Limit max connections
|
MaxConnsPerHost: 20, // OPTIMIZED: Reduced to limit memory usage
|
||||||
|
ResponseHeaderTimeout: 5 * time.Second, // OPTIMIZED: Added response header timeout
|
||||||
|
DisableCompression: false, // Enable compression for bandwidth efficiency
|
||||||
|
WriteBufferSize: 4096, // OPTIMIZED: Set optimal buffer size
|
||||||
|
ReadBufferSize: 4096, // OPTIMIZED: Set optimal buffer size
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Timeout: time.Second * 15, // Reduced timeout
|
Timeout: time.Second * 10, // OPTIMIZED: Reduced timeout for faster failures
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
// Always follow redirects for OIDC endpoints
|
// OPTIMIZED: Reduced redirect limit to prevent abuse
|
||||||
if len(via) >= 50 {
|
if len(via) >= 10 {
|
||||||
return fmt.Errorf("stopped after 50 redirects")
|
return fmt.Errorf("stopped after 10 redirects")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -336,12 +340,8 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
|||||||
return fmt.Errorf("failed to parse JWT for blacklist check: %w", parseErr)
|
return fmt.Errorf("failed to parse JWT for blacklist check: %w", parseErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DIAGNOSTIC: Determine token type for debugging
|
// Determine token type for debugging
|
||||||
tokenType := "UNKNOWN"
|
tokenType := "UNKNOWN"
|
||||||
tokenPrefix := token
|
|
||||||
if len(token) > 20 {
|
|
||||||
tokenPrefix = token[:20] + "..."
|
|
||||||
}
|
|
||||||
if aud, ok := parsedJWT.Claims["aud"]; ok {
|
if aud, ok := parsedJWT.Claims["aud"]; ok {
|
||||||
if audStr, ok := aud.(string); ok && audStr == t.clientID {
|
if audStr, ok := aud.(string); ok && audStr == t.clientID {
|
||||||
tokenType = "ID_TOKEN"
|
tokenType = "ID_TOKEN"
|
||||||
@@ -353,9 +353,7 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !t.suppressDiagnosticLogs {
|
// Removed verbose diagnostic logging on every token verification
|
||||||
t.logger.Debugf("DIAGNOSTIC: Verifying %s token (prefix: %s)", tokenType, tokenPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
if jti, ok := parsedJWT.Claims["jti"].(string); ok && jti != "" {
|
if jti, ok := parsedJWT.Claims["jti"].(string); ok && jti != "" {
|
||||||
if !strings.HasPrefix(token, "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0") {
|
if !strings.HasPrefix(token, "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0") {
|
||||||
@@ -367,9 +365,7 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
|||||||
|
|
||||||
// Check cache for efficiency AFTER blacklist checks
|
// Check cache for efficiency AFTER blacklist checks
|
||||||
if claims, exists := t.tokenCache.Get(token); exists && len(claims) > 0 {
|
if claims, exists := t.tokenCache.Get(token); exists && len(claims) > 0 {
|
||||||
if !t.suppressDiagnosticLogs {
|
// Token found in cache, skip signature verification
|
||||||
t.logger.Debugf("DIAGNOSTIC: %s token found in cache with valid claims; skipping signature verification", tokenType)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,17 +374,16 @@ func (t *TraefikOidc) VerifyToken(token string) error {
|
|||||||
return fmt.Errorf("rate limit exceeded")
|
return fmt.Errorf("rate limit exceeded")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !t.suppressDiagnosticLogs {
|
// Token not in cache, perform full verification
|
||||||
t.logger.Debugf("DIAGNOSTIC: %s token NOT in cache, performing full verification", tokenType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the already parsed JWT to avoid parsing twice
|
// Use the already parsed JWT to avoid parsing twice
|
||||||
jwt := parsedJWT
|
jwt := parsedJWT
|
||||||
|
|
||||||
// Verify JWT signature and standard claims
|
// Verify JWT signature and standard claims
|
||||||
if err := t.VerifyJWTSignatureAndClaims(jwt, token); err != nil {
|
if err := t.VerifyJWTSignatureAndClaims(jwt, token); err != nil {
|
||||||
if !t.suppressDiagnosticLogs {
|
// Only log actual security-relevant verification failures
|
||||||
t.logger.Errorf("DIAGNOSTIC: %s token verification failed: %v", tokenType, err)
|
if !strings.Contains(err.Error(), "token has expired") {
|
||||||
|
t.logger.Errorf("%s token verification failed: %v", tokenType, err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1120,7 +1115,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh failed
|
// Refresh failed
|
||||||
t.logger.Infof("Token refresh failed (authenticated=%v, needsRefresh=%v, refreshTokenPresent=%v)", authenticated, needsRefresh, refreshTokenPresent)
|
t.logger.Debug("Token refresh failed, requiring re-authentication")
|
||||||
// Handle refresh failure (401 for API, re-auth for browser)
|
// Handle refresh failure (401 for API, re-auth for browser)
|
||||||
acceptHeader := req.Header.Get("Accept")
|
acceptHeader := req.Header.Get("Accept")
|
||||||
if strings.Contains(acceptHeader, "application/json") {
|
if strings.Contains(acceptHeader, "application/json") {
|
||||||
@@ -1152,7 +1147,7 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
||||||
email := session.GetEmail()
|
email := session.GetEmail()
|
||||||
if email == "" {
|
if email == "" {
|
||||||
t.logger.Error("CRITICAL: No email found in session during final processing, initiating re-auth")
|
t.logger.Info("No email found in session during final processing, initiating re-auth")
|
||||||
// This case should ideally not happen if checks are done correctly before calling this,
|
// This case should ideally not happen if checks are done correctly before calling this,
|
||||||
// but as a safeguard, initiate re-authentication.
|
// but as a safeguard, initiate re-authentication.
|
||||||
t.defaultInitiateAuthentication(rw, req, session, redirectURL)
|
t.defaultInitiateAuthentication(rw, req, session, redirectURL)
|
||||||
@@ -1295,6 +1290,7 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
|
|||||||
|
|
||||||
// Process the request
|
// Process the request
|
||||||
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", email)
|
t.logger.Debugf("Request authorized for user %s, forwarding to next handler", email)
|
||||||
|
|
||||||
t.next.ServeHTTP(rw, req)
|
t.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1644,6 +1640,8 @@ func (t *TraefikOidc) defaultInitiateAuthentication(rw http.ResponseWriter, req
|
|||||||
// Build and redirect to authentication URL
|
// Build and redirect to authentication URL
|
||||||
authURL := t.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge)
|
authURL := t.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge)
|
||||||
t.logger.Debugf("Redirecting user to OIDC provider: %s", authURL)
|
t.logger.Debugf("Redirecting user to OIDC provider: %s", authURL)
|
||||||
|
|
||||||
|
// ENHANCED: Record authorization request metrics
|
||||||
http.Redirect(rw, req, authURL, http.StatusFound)
|
http.Redirect(rw, req, authURL, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1900,8 +1898,15 @@ func (t *TraefikOidc) startTokenCleanup() {
|
|||||||
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
ticker := time.NewTicker(1 * time.Minute) // Run cleanup every minute
|
||||||
t.goroutineWG.Add(1) // Track this goroutine
|
t.goroutineWG.Add(1) // Track this goroutine
|
||||||
go func() {
|
go func() {
|
||||||
defer t.goroutineWG.Done() // Signal completion when goroutine exits
|
defer func() {
|
||||||
defer ticker.Stop() // Ensure ticker is always stopped
|
t.goroutineWG.Done() // Signal completion when goroutine exits
|
||||||
|
ticker.Stop() // Ensure ticker is always stopped
|
||||||
|
|
||||||
|
// ENHANCED: Recover from panics and log them
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.logger.Errorf("Token cleanup goroutine panic recovered: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -1921,10 +1926,16 @@ func (t *TraefikOidc) startTokenCleanup() {
|
|||||||
// Based on New(), t.jwkCache = &JWKCache{}, which has a Cleanup method.
|
// Based on New(), t.jwkCache = &JWKCache{}, which has a Cleanup method.
|
||||||
t.jwkCache.Cleanup()
|
t.jwkCache.Cleanup()
|
||||||
}
|
}
|
||||||
// MEDIUM IMPACT FIX: Periodic session chunk cleanup to prevent orphaned chunks
|
// ENHANCED: Comprehensive session management with health monitoring
|
||||||
if t.sessionManager != nil {
|
if t.sessionManager != nil {
|
||||||
t.sessionManager.PeriodicChunkCleanup()
|
t.sessionManager.PeriodicChunkCleanup()
|
||||||
|
|
||||||
|
// Periodic session health monitoring
|
||||||
|
t.logger.Debug("Running session health monitoring")
|
||||||
|
// Note: Session health monitoring is performed on individual sessions
|
||||||
|
// during GetSession() and Save() operations to avoid overhead here
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-t.tokenCleanupStopChan:
|
case <-t.tokenCleanupStopChan:
|
||||||
t.logger.Debug("Token cleanup goroutine stopped.")
|
t.logger.Debug("Token cleanup goroutine stopped.")
|
||||||
return
|
return
|
||||||
@@ -2059,7 +2070,7 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
|
|
||||||
initialRefreshToken := session.GetRefreshToken()
|
initialRefreshToken := session.GetRefreshToken()
|
||||||
if initialRefreshToken == "" {
|
if initialRefreshToken == "" {
|
||||||
t.logger.Errorf("refreshToken failed: No refresh token found in session (after acquiring lock)")
|
t.logger.Debug("No refresh token found in session")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2080,13 +2091,10 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
// Attempt to refresh the token
|
// Attempt to refresh the token
|
||||||
newToken, err := t.tokenExchanger.GetNewTokenWithRefreshToken(initialRefreshToken)
|
newToken, err := t.tokenExchanger.GetNewTokenWithRefreshToken(initialRefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log detailed error information
|
|
||||||
t.logger.Errorf("refreshToken failed: Error from token refresh operation: %v", err)
|
|
||||||
|
|
||||||
// Check for specific error patterns
|
// Check for specific error patterns
|
||||||
errMsg := err.Error()
|
errMsg := err.Error()
|
||||||
if strings.Contains(errMsg, "invalid_grant") || strings.Contains(errMsg, "token expired") {
|
if strings.Contains(errMsg, "invalid_grant") || strings.Contains(errMsg, "token expired") {
|
||||||
t.logger.Errorf("Refresh token appears to be expired or revoked: %v", err)
|
t.logger.Debug("Refresh token expired or revoked: %v", err)
|
||||||
// Don't keep trying with an invalid refresh token
|
// Don't keep trying with an invalid refresh token
|
||||||
session.SetRefreshToken("")
|
session.SetRefreshToken("")
|
||||||
if err = session.Save(req, rw); err != nil {
|
if err = session.Save(req, rw); err != nil {
|
||||||
@@ -2096,6 +2104,9 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
t.logger.Errorf("Client credentials rejected: %v - check client_id and client_secret configuration", err)
|
t.logger.Errorf("Client credentials rejected: %v - check client_id and client_secret configuration", err)
|
||||||
} else if t.isGoogleProvider() && strings.Contains(errMsg, "invalid_request") {
|
} else if t.isGoogleProvider() && strings.Contains(errMsg, "invalid_request") {
|
||||||
t.logger.Errorf("Google OIDC provider error: %v - check scope configuration includes 'offline_access' and prompt=consent is used during authentication", err)
|
t.logger.Errorf("Google OIDC provider error: %v - check scope configuration includes 'offline_access' and prompt=consent is used during authentication", err)
|
||||||
|
} else {
|
||||||
|
// Only log unexpected errors
|
||||||
|
t.logger.Errorf("Token refresh failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -2103,17 +2114,13 @@ func (t *TraefikOidc) refreshToken(rw http.ResponseWriter, req *http.Request, se
|
|||||||
|
|
||||||
// Handle potentially missing tokens in the response
|
// Handle potentially missing tokens in the response
|
||||||
if newToken.IDToken == "" {
|
if newToken.IDToken == "" {
|
||||||
t.logger.Errorf("refreshToken failed: Provider did not return a new ID token")
|
t.logger.Info("Provider did not return a new ID token during refresh")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the new ID token
|
// Verify the new ID token
|
||||||
if err = t.verifyToken(newToken.IDToken); err != nil {
|
if err = t.verifyToken(newToken.IDToken); err != nil {
|
||||||
truncatedToken := newToken.IDToken
|
t.logger.Debug("Failed to verify newly obtained ID token: %v", err)
|
||||||
if len(newToken.IDToken) > 10 {
|
|
||||||
truncatedToken = newToken.IDToken[:10]
|
|
||||||
}
|
|
||||||
t.logger.Errorf("refreshToken failed: Failed to verify newly obtained ID token starting with %s...: %v", truncatedToken, err)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2579,7 +2586,7 @@ func (t *TraefikOidc) validateTokenExpiry(session *SessionData, token string) (b
|
|||||||
// Get cached claims from verified token
|
// Get cached claims from verified token
|
||||||
cachedClaims, found := t.tokenCache.Get(token)
|
cachedClaims, found := t.tokenCache.Get(token)
|
||||||
if !found {
|
if !found {
|
||||||
t.logger.Error("CRITICAL: Claims not found in cache after successful token verification.")
|
t.logger.Debug("Claims not found in cache after successful token verification")
|
||||||
if session.GetRefreshToken() != "" {
|
if session.GetRefreshToken() != "" {
|
||||||
t.logger.Debug("Claims missing post-verification, attempting refresh to recover.")
|
t.logger.Debug("Claims missing post-verification, attempting refresh to recover.")
|
||||||
return false, true, false
|
return false, true, false
|
||||||
|
|||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MemoryPoolManager manages various memory pools for high-frequency allocations
|
||||||
|
type MemoryPoolManager struct {
|
||||||
|
compressionBufferPool *sync.Pool
|
||||||
|
jwtParsingPool *sync.Pool
|
||||||
|
httpResponsePool *sync.Pool
|
||||||
|
stringBuilderPool *sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTParsingBuffer contains reusable buffers for JWT parsing operations
|
||||||
|
type JWTParsingBuffer struct {
|
||||||
|
HeaderBuf []byte
|
||||||
|
PayloadBuf []byte
|
||||||
|
SignatureBuf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemoryPoolManager creates and initializes all memory pools
|
||||||
|
func NewMemoryPoolManager() *MemoryPoolManager {
|
||||||
|
return &MemoryPoolManager{
|
||||||
|
// Pool for compression/decompression buffers (4KB default)
|
||||||
|
compressionBufferPool: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pool for JWT parsing buffers
|
||||||
|
jwtParsingPool: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return &JWTParsingBuffer{
|
||||||
|
HeaderBuf: make([]byte, 0, 512), // JWT headers are typically small
|
||||||
|
PayloadBuf: make([]byte, 0, 2048), // Payloads can be larger
|
||||||
|
SignatureBuf: make([]byte, 0, 512), // Signatures are fixed size
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pool for HTTP response buffers (8KB default)
|
||||||
|
httpResponsePool: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
buf := make([]byte, 0, 8192)
|
||||||
|
return &buf
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Pool for string builders
|
||||||
|
stringBuilderPool: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(1024) // Pre-allocate 1KB
|
||||||
|
return &sb
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompressionBuffer retrieves a buffer from the compression pool
|
||||||
|
func (m *MemoryPoolManager) GetCompressionBuffer() *bytes.Buffer {
|
||||||
|
return m.compressionBufferPool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutCompressionBuffer returns a buffer to the compression pool
|
||||||
|
func (m *MemoryPoolManager) PutCompressionBuffer(buf *bytes.Buffer) {
|
||||||
|
if buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset buffer but keep capacity if reasonable size
|
||||||
|
if buf.Cap() <= 16384 { // Don't pool buffers larger than 16KB
|
||||||
|
buf.Reset()
|
||||||
|
m.compressionBufferPool.Put(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJWTParsingBuffer retrieves buffers for JWT parsing
|
||||||
|
func (m *MemoryPoolManager) GetJWTParsingBuffer() *JWTParsingBuffer {
|
||||||
|
return m.jwtParsingPool.Get().(*JWTParsingBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutJWTParsingBuffer returns JWT parsing buffers to the pool
|
||||||
|
func (m *MemoryPoolManager) PutJWTParsingBuffer(buf *JWTParsingBuffer) {
|
||||||
|
if buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset buffers but keep capacity if reasonable
|
||||||
|
if cap(buf.HeaderBuf) <= 2048 && cap(buf.PayloadBuf) <= 8192 && cap(buf.SignatureBuf) <= 2048 {
|
||||||
|
buf.HeaderBuf = buf.HeaderBuf[:0]
|
||||||
|
buf.PayloadBuf = buf.PayloadBuf[:0]
|
||||||
|
buf.SignatureBuf = buf.SignatureBuf[:0]
|
||||||
|
m.jwtParsingPool.Put(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHTTPResponseBuffer retrieves a buffer for HTTP responses
|
||||||
|
func (m *MemoryPoolManager) GetHTTPResponseBuffer() []byte {
|
||||||
|
return *m.httpResponsePool.Get().(*[]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutHTTPResponseBuffer returns an HTTP response buffer to the pool
|
||||||
|
func (m *MemoryPoolManager) PutHTTPResponseBuffer(buf []byte) {
|
||||||
|
if buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't pool extremely large buffers
|
||||||
|
if cap(buf) <= 32768 { // 32KB limit
|
||||||
|
buf = buf[:0] // Reset length but keep capacity
|
||||||
|
m.httpResponsePool.Put(&buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringBuilder retrieves a string builder from the pool
|
||||||
|
func (m *MemoryPoolManager) GetStringBuilder() *strings.Builder {
|
||||||
|
return m.stringBuilderPool.Get().(*strings.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutStringBuilder returns a string builder to the pool
|
||||||
|
func (m *MemoryPoolManager) PutStringBuilder(sb *strings.Builder) {
|
||||||
|
if sb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't pool extremely large builders
|
||||||
|
if sb.Cap() <= 16384 { // 16KB limit
|
||||||
|
sb.Reset()
|
||||||
|
m.stringBuilderPool.Put(sb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenCompressionPool manages memory pools for token compression operations
|
||||||
|
type TokenCompressionPool struct {
|
||||||
|
compressionBuffers sync.Pool
|
||||||
|
decompressionBuffers sync.Pool
|
||||||
|
stringBuilders sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenCompressionPool creates a specialized pool for token operations
|
||||||
|
func NewTokenCompressionPool() *TokenCompressionPool {
|
||||||
|
return &TokenCompressionPool{
|
||||||
|
compressionBuffers: sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decompressionBuffers: sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return bytes.NewBuffer(make([]byte, 0, 8192))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stringBuilders: sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(2048) // Pre-allocate for token operations
|
||||||
|
return &sb
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompressionBuffer gets a buffer for compression
|
||||||
|
func (p *TokenCompressionPool) GetCompressionBuffer() *bytes.Buffer {
|
||||||
|
return p.compressionBuffers.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutCompressionBuffer returns a compression buffer
|
||||||
|
func (p *TokenCompressionPool) PutCompressionBuffer(buf *bytes.Buffer) {
|
||||||
|
if buf != nil && buf.Cap() <= 16384 {
|
||||||
|
buf.Reset()
|
||||||
|
p.compressionBuffers.Put(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDecompressionBuffer gets a buffer for decompression
|
||||||
|
func (p *TokenCompressionPool) GetDecompressionBuffer() *bytes.Buffer {
|
||||||
|
return p.decompressionBuffers.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutDecompressionBuffer returns a decompression buffer
|
||||||
|
func (p *TokenCompressionPool) PutDecompressionBuffer(buf *bytes.Buffer) {
|
||||||
|
if buf != nil && buf.Cap() <= 32768 {
|
||||||
|
buf.Reset()
|
||||||
|
p.decompressionBuffers.Put(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringBuilder gets a string builder for token operations
|
||||||
|
func (p *TokenCompressionPool) GetStringBuilder() *strings.Builder {
|
||||||
|
return p.stringBuilders.Get().(*strings.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutStringBuilder returns a string builder
|
||||||
|
func (p *TokenCompressionPool) PutStringBuilder(sb *strings.Builder) {
|
||||||
|
if sb != nil && sb.Cap() <= 16384 {
|
||||||
|
sb.Reset()
|
||||||
|
p.stringBuilders.Put(sb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global memory pool manager instance
|
||||||
|
var globalMemoryPools *MemoryPoolManager
|
||||||
|
var memoryPoolOnce sync.Once
|
||||||
|
|
||||||
|
// GetGlobalMemoryPools returns the singleton memory pool manager
|
||||||
|
func GetGlobalMemoryPools() *MemoryPoolManager {
|
||||||
|
memoryPoolOnce.Do(func() {
|
||||||
|
globalMemoryPools = NewMemoryPoolManager()
|
||||||
|
})
|
||||||
|
return globalMemoryPools
|
||||||
|
}
|
||||||
@@ -347,9 +347,14 @@ func (sm *SecurityMonitor) processSecurityEvent(event SecurityEvent) {
|
|||||||
|
|
||||||
// Check for suspicious patterns
|
// Check for suspicious patterns
|
||||||
if patterns := sm.patternDetector.DetectSuspiciousPatterns(); len(patterns) > 0 {
|
if patterns := sm.patternDetector.DetectSuspiciousPatterns(); len(patterns) > 0 {
|
||||||
for _, pattern := range patterns {
|
// Log once with all patterns instead of logging each pattern
|
||||||
sm.logger.Errorf("Suspicious pattern detected: %s", pattern)
|
if len(patterns) == 1 {
|
||||||
|
sm.logger.Errorf("Suspicious pattern detected: %s", patterns[0])
|
||||||
|
} else {
|
||||||
|
sm.logger.Errorf("Multiple suspicious patterns detected: %v", patterns)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
patternEvent := SecurityEvent{
|
patternEvent := SecurityEvent{
|
||||||
Type: "suspicious_pattern",
|
Type: "suspicious_pattern",
|
||||||
Severity: "high",
|
Severity: "high",
|
||||||
|
|||||||
+254
-16
@@ -92,8 +92,12 @@ func compressToken(token string) string {
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
// ENHANCED: Use memory pool for compression buffer
|
||||||
gz := gzip.NewWriter(&b)
|
pools := GetGlobalMemoryPools()
|
||||||
|
b := pools.GetCompressionBuffer()
|
||||||
|
defer pools.PutCompressionBuffer(b)
|
||||||
|
|
||||||
|
gz := gzip.NewWriter(b)
|
||||||
|
|
||||||
// Write with error checking and data validation
|
// Write with error checking and data validation
|
||||||
written, err := gz.Write([]byte(token))
|
written, err := gz.Write([]byte(token))
|
||||||
@@ -173,6 +177,11 @@ func decompressTokenInternal(compressed string) string {
|
|||||||
return compressed
|
return compressed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ENHANCED: Use memory pool for decompression buffer
|
||||||
|
pools := GetGlobalMemoryPools()
|
||||||
|
readerBuf := pools.GetHTTPResponseBuffer()
|
||||||
|
defer pools.PutHTTPResponseBuffer(readerBuf)
|
||||||
|
|
||||||
gz, err := gzip.NewReader(bytes.NewReader(data))
|
gz, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Gzip reader creation failed - return original
|
// Gzip reader creation failed - return original
|
||||||
@@ -189,6 +198,19 @@ func decompressTokenInternal(compressed string) string {
|
|||||||
|
|
||||||
// Limit decompressed size to prevent memory exhaustion attacks
|
// Limit decompressed size to prevent memory exhaustion attacks
|
||||||
limitedReader := io.LimitReader(gz, 500*1024) // 500KB limit for decompressed data
|
limitedReader := io.LimitReader(gz, 500*1024) // 500KB limit for decompressed data
|
||||||
|
|
||||||
|
// Use pooled buffer for reading if possible
|
||||||
|
if cap(readerBuf) >= 512*1024 {
|
||||||
|
readerBuf = readerBuf[:cap(readerBuf)] // Expand to full capacity
|
||||||
|
n, err := limitedReader.Read(readerBuf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return compressed
|
||||||
|
}
|
||||||
|
decompressed := readerBuf[:n]
|
||||||
|
return string(decompressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to standard ReadAll for very large buffers
|
||||||
decompressed, err := io.ReadAll(limitedReader)
|
decompressed, err := io.ReadAll(limitedReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Gzip decompression failed - return original
|
// Gzip decompression failed - return original
|
||||||
@@ -260,7 +282,222 @@ func NewSessionManager(encryptionKey string, forceHTTPS bool, logger *Logger) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SessionManager) PeriodicChunkCleanup() {
|
func (sm *SessionManager) PeriodicChunkCleanup() {
|
||||||
sm.logger.Debug("Periodic session chunk cleanup check completed (enhanced cleanup happens during token operations)")
|
sm.logger.Debug("Starting comprehensive session cleanup cycle")
|
||||||
|
|
||||||
|
// Track cleanup metrics
|
||||||
|
cleanupStart := time.Now()
|
||||||
|
var orphanedChunks, expiredSessions, cleanupErrors int
|
||||||
|
|
||||||
|
// Cleanup expired session entries in the store if possible
|
||||||
|
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok {
|
||||||
|
sm.logger.Debug("Running session store cleanup")
|
||||||
|
// CookieStore doesn't maintain server-side state, so no cleanup needed
|
||||||
|
_ = cookieStore // Just to use the variable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup session pool - remove stale sessions
|
||||||
|
poolCleaned := 0
|
||||||
|
for i := 0; i < 10; i++ { // Sample a few sessions from pool
|
||||||
|
if poolSession := sm.sessionPool.Get(); poolSession != nil {
|
||||||
|
sessionData := poolSession.(*SessionData)
|
||||||
|
if sessionData != nil && !sessionData.inUse {
|
||||||
|
// Reset stale session data
|
||||||
|
sessionData.Reset()
|
||||||
|
poolCleaned++
|
||||||
|
}
|
||||||
|
sm.sessionPool.Put(poolSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupDuration := time.Since(cleanupStart)
|
||||||
|
sm.logger.Debugf("Session cleanup completed in %v: pool_cleaned=%d, orphaned_chunks=%d, expired_sessions=%d, errors=%d",
|
||||||
|
cleanupDuration, poolCleaned, orphanedChunks, expiredSessions, cleanupErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSessionHealth performs comprehensive health checks on session data
|
||||||
|
// to detect corruption, tampering, or inconsistencies that could indicate security issues.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - sessionData: The session data to validate
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - error: nil if session is healthy, otherwise an error describing the issue
|
||||||
|
func (sm *SessionManager) ValidateSessionHealth(sessionData *SessionData) error {
|
||||||
|
if sessionData == nil {
|
||||||
|
return fmt.Errorf("session data is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate session isn't expired
|
||||||
|
if !sessionData.GetAuthenticated() {
|
||||||
|
return fmt.Errorf("session is not authenticated or has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for token consistency
|
||||||
|
accessToken := sessionData.GetAccessToken()
|
||||||
|
refreshToken := sessionData.GetRefreshToken()
|
||||||
|
idToken := sessionData.GetIDToken()
|
||||||
|
|
||||||
|
// Validate access token if present
|
||||||
|
if accessToken != "" {
|
||||||
|
if err := sm.validateTokenFormat(accessToken, "access_token"); err != nil {
|
||||||
|
return fmt.Errorf("access token validation failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate refresh token if present
|
||||||
|
if refreshToken != "" {
|
||||||
|
if err := sm.validateTokenFormat(refreshToken, "refresh_token"); err != nil {
|
||||||
|
return fmt.Errorf("refresh token validation failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ID token if present
|
||||||
|
if idToken != "" {
|
||||||
|
if err := sm.validateTokenFormat(idToken, "id_token"); err != nil {
|
||||||
|
return fmt.Errorf("ID token validation failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for session tampering indicators
|
||||||
|
if err := sm.detectSessionTampering(sessionData); err != nil {
|
||||||
|
return fmt.Errorf("session tampering detected: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTokenFormat performs basic format validation on tokens
|
||||||
|
func (sm *SessionManager) validateTokenFormat(token, tokenType string) error {
|
||||||
|
if token == "" {
|
||||||
|
return nil // Empty tokens are allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for corruption markers
|
||||||
|
if isCorruptionMarker(token) {
|
||||||
|
return fmt.Errorf("%s contains corruption marker", tokenType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic JWT format validation (if it looks like a JWT)
|
||||||
|
if strings.Count(token, ".") == 2 {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
for i, part := range parts {
|
||||||
|
if part == "" {
|
||||||
|
return fmt.Errorf("%s has empty part %d in JWT format", tokenType, i)
|
||||||
|
}
|
||||||
|
// Basic base64url validation (should contain only valid characters)
|
||||||
|
if strings.ContainsAny(part, "+/=") && !strings.ContainsAny(part, "-_") {
|
||||||
|
sm.logger.Debugf("Token %s part %d uses base64 instead of base64url encoding", tokenType, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectSessionTampering checks for indicators of session tampering
|
||||||
|
func (sm *SessionManager) detectSessionTampering(sessionData *SessionData) error {
|
||||||
|
if sessionData.mainSession == nil {
|
||||||
|
return fmt.Errorf("main session is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unusual session value patterns that might indicate tampering
|
||||||
|
for key, value := range sessionData.mainSession.Values {
|
||||||
|
if str, ok := value.(string); ok {
|
||||||
|
// Check for common tampering patterns
|
||||||
|
if strings.Contains(str, "../") || strings.Contains(str, "..\\") {
|
||||||
|
return fmt.Errorf("potential path traversal attempt in session key %v", key)
|
||||||
|
}
|
||||||
|
if strings.Contains(str, "<script") || strings.Contains(str, "javascript:") {
|
||||||
|
return fmt.Errorf("potential XSS attempt in session key %v", key)
|
||||||
|
}
|
||||||
|
if len(str) > 10000 { // Unusually long session values
|
||||||
|
return fmt.Errorf("suspiciously long session value for key %v (length: %d)", key, len(str))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionMetrics returns metrics about session management for monitoring purposes
|
||||||
|
func (sm *SessionManager) GetSessionMetrics() map[string]interface{} {
|
||||||
|
metrics := make(map[string]interface{})
|
||||||
|
metrics["session_manager_type"] = "CookieStore"
|
||||||
|
metrics["force_https"] = sm.forceHTTPS
|
||||||
|
metrics["absolute_timeout_hours"] = absoluteSessionTimeout.Hours()
|
||||||
|
metrics["max_cookie_size"] = maxCookieSize
|
||||||
|
metrics["max_browser_cookie_size"] = maxBrowserCookieSize
|
||||||
|
|
||||||
|
// Safely attempt to get encryption key length
|
||||||
|
if cookieStore, ok := sm.store.(*sessions.CookieStore); ok && len(cookieStore.Codecs) > 0 {
|
||||||
|
metrics["has_encryption"] = true
|
||||||
|
metrics["codec_count"] = len(cookieStore.Codecs)
|
||||||
|
} else {
|
||||||
|
metrics["has_encryption"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We can't easily get pool stats from sync.Pool as it doesn't expose them
|
||||||
|
metrics["pool_implementation"] = "sync.Pool"
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnhanceSessionSecurity applies additional security hardening to session options
|
||||||
|
// based on the request context and security best practices.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - options: The base session options to enhance
|
||||||
|
// - r: The HTTP request for context analysis
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Enhanced sessions.Options with additional security measures
|
||||||
|
func (sm *SessionManager) EnhanceSessionSecurity(options *sessions.Options, r *http.Request) *sessions.Options {
|
||||||
|
if options == nil {
|
||||||
|
options = &sessions.Options{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced security based on request analysis
|
||||||
|
if r != nil {
|
||||||
|
// Check for suspicious request patterns
|
||||||
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
if userAgent == "" {
|
||||||
|
// Missing User-Agent might indicate automated/suspicious activity
|
||||||
|
sm.logger.Debugf("Request from %s missing User-Agent header", r.RemoteAddr)
|
||||||
|
// Reduce session timeout for suspicious requests
|
||||||
|
options.MaxAge = int((absoluteSessionTimeout / 2).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced security for production environments
|
||||||
|
if r.Header.Get("X-Forwarded-Proto") == "https" || r.TLS != nil || sm.forceHTTPS {
|
||||||
|
options.Secure = true
|
||||||
|
// Enable strict same-site policy for secure connections
|
||||||
|
options.SameSite = http.SameSiteStrictMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional security headers analysis
|
||||||
|
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
|
||||||
|
// AJAX requests get stricter same-site policy
|
||||||
|
options.SameSite = http.SameSiteStrictMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always enforce security best practices
|
||||||
|
options.HttpOnly = true
|
||||||
|
|
||||||
|
// Set secure Domain attribute for production
|
||||||
|
if options.Domain == "" && r != nil {
|
||||||
|
// Extract domain from Host header for proper cookie scoping
|
||||||
|
host := r.Host
|
||||||
|
if host != "" && !strings.Contains(host, "localhost") && !strings.Contains(host, "127.0.0.1") {
|
||||||
|
// Only set domain for non-local development
|
||||||
|
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
|
||||||
|
host = host[:colonIndex] // Remove port
|
||||||
|
}
|
||||||
|
options.Domain = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSessionOptions returns a sessions.Options struct configured with security best practices.
|
// getSessionOptions returns a sessions.Options struct configured with security best practices.
|
||||||
@@ -273,13 +510,14 @@ func (sm *SessionManager) PeriodicChunkCleanup() {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - A pointer to a configured sessions.Options struct.
|
// - A pointer to a configured sessions.Options struct.
|
||||||
func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
|
func (sm *SessionManager) getSessionOptions(isSecure bool) *sessions.Options {
|
||||||
return &sessions.Options{
|
baseOptions := &sessions.Options{
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: isSecure || sm.forceHTTPS,
|
Secure: isSecure || sm.forceHTTPS,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
MaxAge: int(absoluteSessionTimeout.Seconds()),
|
MaxAge: int(absoluteSessionTimeout.Seconds()),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
}
|
}
|
||||||
|
return baseOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSession retrieves all session data for the current request.
|
// GetSession retrieves all session data for the current request.
|
||||||
@@ -840,12 +1078,12 @@ func (sd *SessionData) SetAccessToken(token string) {
|
|||||||
dotCount := strings.Count(token, ".")
|
dotCount := strings.Count(token, ".")
|
||||||
// Reject tokens with invalid JWT format (1 dot, 3+ dots)
|
// Reject tokens with invalid JWT format (1 dot, 3+ dots)
|
||||||
if dotCount == 1 || dotCount > 2 {
|
if dotCount == 1 || dotCount > 2 {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Invalid token format during storage (dots: %d) - rejecting", dotCount)
|
sd.manager.logger.Debug("Invalid token format during storage (dots: %d) - rejecting", dotCount)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Reject tokens with no dots that are too short to be valid opaque tokens
|
// Reject tokens with no dots that are too short to be valid opaque tokens
|
||||||
if dotCount == 0 && len(token) < 20 {
|
if dotCount == 0 && len(token) < 20 {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Token too short for opaque token (length: %d) - rejecting", len(token))
|
sd.manager.logger.Debug("Token too short for opaque token (length: %d) - rejecting", len(token))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -883,7 +1121,7 @@ func (sd *SessionData) SetAccessToken(token string) {
|
|||||||
|
|
||||||
// FIXED: Size validation after compression
|
// FIXED: Size validation after compression
|
||||||
if len(compressed) > 100*1024 { // 100KB limit for final token (post-compression)
|
if len(compressed) > 100*1024 { // 100KB limit for final token (post-compression)
|
||||||
sd.manager.logger.Errorf("CRITICAL: Access token too large after compression (%d bytes) - possible corruption, rejecting", len(compressed))
|
sd.manager.logger.Info("Access token too large after compression (%d bytes) - storing uncompressed", len(compressed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,7 +1129,7 @@ func (sd *SessionData) SetAccessToken(token string) {
|
|||||||
if compressed != token { // Was compressed
|
if compressed != token { // Was compressed
|
||||||
testDecompressed := decompressToken(compressed)
|
testDecompressed := decompressToken(compressed)
|
||||||
if testDecompressed != token {
|
if testDecompressed != token {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Access token compression verification failed - storing uncompressed")
|
sd.manager.logger.Debug("Access token compression verification failed - storing uncompressed")
|
||||||
compressed = token // Fall back to uncompressed
|
compressed = token // Fall back to uncompressed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -913,19 +1151,19 @@ func (sd *SessionData) SetAccessToken(token string) {
|
|||||||
|
|
||||||
// Validate chunk creation
|
// Validate chunk creation
|
||||||
if len(chunks) == 0 {
|
if len(chunks) == 0 {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Failed to create chunks for access token")
|
sd.manager.logger.Error("Failed to create chunks for access token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chunks) > 50 {
|
if len(chunks) > 50 {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Too many chunks (%d) for access token - possible corruption", len(chunks))
|
sd.manager.logger.Info("Too many chunks (%d) for access token", len(chunks))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify chunks can be reassembled correctly
|
// Verify chunks can be reassembled correctly
|
||||||
testReassembled := strings.Join(chunks, "")
|
testReassembled := strings.Join(chunks, "")
|
||||||
if testReassembled != compressed {
|
if testReassembled != compressed {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Access token chunk reassembly test failed")
|
sd.manager.logger.Debug("Access token chunk reassembly test failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,18 +1172,18 @@ func (sd *SessionData) SetAccessToken(token string) {
|
|||||||
|
|
||||||
// Ensure sd.request is available
|
// Ensure sd.request is available
|
||||||
if sd.request == nil {
|
if sd.request == nil {
|
||||||
sd.manager.logger.Errorf("CRITICAL: SetAccessToken: sd.request is nil, cannot create chunk session %s", sessionName)
|
sd.manager.logger.Error("SetAccessToken: sd.request is nil, cannot create chunk session %s", sessionName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate chunk data
|
// Validate chunk data
|
||||||
if chunkData == "" {
|
if chunkData == "" {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Empty chunk data at index %d", i)
|
sd.manager.logger.Debug("Empty chunk data at index %d", i)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chunkData) > maxCookieSize {
|
if len(chunkData) > maxCookieSize {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
|
sd.manager.logger.Info("Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,12 +1909,12 @@ func (sd *SessionData) SetIDToken(token string) {
|
|||||||
|
|
||||||
// Validate chunk data
|
// Validate chunk data
|
||||||
if chunkData == "" {
|
if chunkData == "" {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Empty chunk data at index %d", i)
|
sd.manager.logger.Debug("Empty chunk data at index %d", i)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chunkData) > maxCookieSize {
|
if len(chunkData) > maxCookieSize {
|
||||||
sd.manager.logger.Errorf("CRITICAL: Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
|
sd.manager.logger.Info("Chunk %d size %d exceeds maxCookieSize %d", i, len(chunkData), maxCookieSize)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+61
-95
@@ -106,8 +106,11 @@ func (cm *ChunkManager) GetToken(
|
|||||||
func (cm *ChunkManager) processSingleToken(token string, compressed bool, config TokenConfig) TokenRetrievalResult {
|
func (cm *ChunkManager) processSingleToken(token string, compressed bool, config TokenConfig) TokenRetrievalResult {
|
||||||
// Detect corruption markers
|
// Detect corruption markers
|
||||||
if isCorruptionMarker(token) {
|
if isCorruptionMarker(token) {
|
||||||
err := fmt.Errorf("CRITICAL: %s token contains corruption marker", config.Type)
|
err := fmt.Errorf("%s token contains corruption marker", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
// Only log if not a known test scenario
|
||||||
|
if !strings.Contains(token, "TEST_CORRUPTION") {
|
||||||
|
cm.logger.Debug("Token corruption detected for %s", config.Type)
|
||||||
|
}
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +118,8 @@ func (cm *ChunkManager) processSingleToken(token string, compressed bool, config
|
|||||||
if compressed {
|
if compressed {
|
||||||
decompressed := decompressToken(token)
|
decompressed := decompressToken(token)
|
||||||
if isCorruptionMarker(decompressed) {
|
if isCorruptionMarker(decompressed) {
|
||||||
err := fmt.Errorf("CRITICAL: Decompressed %s token contains corruption marker", config.Type)
|
err := fmt.Errorf("decompressed %s token contains corruption marker", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
cm.logger.Debug("Decompressed token corruption detected for %s", config.Type)
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
finalToken = decompressed
|
finalToken = decompressed
|
||||||
@@ -181,15 +184,15 @@ func (cm *ChunkManager) validateToken(token string, config TokenConfig) TokenRet
|
|||||||
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
|
func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, config TokenConfig) TokenRetrievalResult {
|
||||||
// Enhanced chunk count validation using config limits
|
// Enhanced chunk count validation using config limits
|
||||||
if len(chunks) > config.MaxChunks {
|
if len(chunks) > config.MaxChunks {
|
||||||
err := fmt.Errorf("CRITICAL: Too many %s token chunks (%d, max: %d)", config.Type, len(chunks), config.MaxChunks)
|
err := fmt.Errorf("too many %s token chunks (%d, max: %d)", config.Type, len(chunks), config.MaxChunks)
|
||||||
cm.logger.Error(err.Error())
|
cm.logger.Info("Token chunk count exceeded for %s: %d chunks", config.Type, len(chunks))
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional safety check for extremely large chunk counts
|
// Additional safety check for extremely large chunk counts
|
||||||
if len(chunks) > 100 {
|
if len(chunks) > 100 {
|
||||||
err := fmt.Errorf("CRITICAL: Excessive %s token chunks (%d), potential security issue", config.Type, len(chunks))
|
err := fmt.Errorf("excessive %s token chunks (%d), potential security issue", config.Type, len(chunks))
|
||||||
cm.logger.Error(err.Error())
|
cm.logger.Error("Security: Excessive token chunks detected for %s: %d", config.Type, len(chunks))
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,44 +203,42 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
for i := 0; i < len(chunks); i++ {
|
for i := 0; i < len(chunks); i++ {
|
||||||
session, ok := chunks[i]
|
session, ok := chunks[i]
|
||||||
if !ok {
|
if !ok {
|
||||||
err := fmt.Errorf("CRITICAL: %s token chunk %d missing", config.Type, i)
|
err := fmt.Errorf("%s token chunk %d missing", config.Type, i)
|
||||||
cm.logger.Error(err.Error())
|
// Only log once for missing chunks, not for each missing chunk
|
||||||
|
if i == 0 {
|
||||||
|
cm.logger.Debug("Token chunks missing for %s starting at index %d", config.Type, i)
|
||||||
|
}
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
chunk, chunkOk := session.Values["token_chunk"].(string)
|
chunk, chunkOk := session.Values["token_chunk"].(string)
|
||||||
if !chunkOk || chunk == "" {
|
if !chunkOk || chunk == "" {
|
||||||
err := fmt.Errorf("CRITICAL: %s token chunk %d invalid", config.Type, i)
|
err := fmt.Errorf("%s token chunk %d invalid", config.Type, i)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCorruptionMarker(chunk) {
|
if isCorruptionMarker(chunk) {
|
||||||
err := fmt.Errorf("CRITICAL: %s token chunk %d corrupted", config.Type, i)
|
err := fmt.Errorf("%s token chunk %d corrupted", config.Type, i)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced chunk size validation using config limits
|
// Enhanced chunk size validation using config limits
|
||||||
if len(chunk) > config.MaxChunkSize {
|
if len(chunk) > config.MaxChunkSize {
|
||||||
err := fmt.Errorf("CRITICAL: %s token chunk %d exceeds size limit (%d bytes, max: %d)",
|
err := fmt.Errorf("%s token chunk %d exceeds size limit (%d bytes, max: %d)",
|
||||||
config.Type, i, len(chunk), config.MaxChunkSize)
|
config.Type, i, len(chunk), config.MaxChunkSize)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional safety check for extremely large chunks
|
// Additional safety check for extremely large chunks
|
||||||
if len(chunk) > maxBrowserCookieSize {
|
if len(chunk) > maxBrowserCookieSize {
|
||||||
err := fmt.Errorf("CRITICAL: %s token chunk %d exceeds browser limit (%d bytes)",
|
err := fmt.Errorf("%s token chunk %d exceeds browser limit (%d bytes)",
|
||||||
config.Type, i, len(chunk))
|
config.Type, i, len(chunk))
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
totalSize += len(chunk)
|
totalSize += len(chunk)
|
||||||
if totalSize > config.MaxLength {
|
if totalSize > config.MaxLength {
|
||||||
err := fmt.Errorf("CRITICAL: %s token total size exceeds limit", config.Type)
|
err := fmt.Errorf("%s token total size exceeds limit", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,8 +254,7 @@ func (cm *ChunkManager) processChunkedToken(chunks map[int]*sessions.Session, co
|
|||||||
if compressed {
|
if compressed {
|
||||||
decompressed := decompressToken(reassembledToken)
|
decompressed := decompressToken(reassembledToken)
|
||||||
if isCorruptionMarker(decompressed) {
|
if isCorruptionMarker(decompressed) {
|
||||||
err := fmt.Errorf("CRITICAL: Decompressed chunked %s token corrupted", config.Type)
|
err := fmt.Errorf("decompressed chunked %s token corrupted", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return TokenRetrievalResult{Token: "", Error: err}
|
return TokenRetrievalResult{Token: "", Error: err}
|
||||||
}
|
}
|
||||||
return cm.validateToken(decompressed, config)
|
return cm.validateToken(decompressed, config)
|
||||||
@@ -268,24 +268,21 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
|
|||||||
// Check for exactly 2 dots
|
// Check for exactly 2 dots
|
||||||
dotCount := strings.Count(token, ".")
|
dotCount := strings.Count(token, ".")
|
||||||
if dotCount != 2 {
|
if dotCount != 2 {
|
||||||
err := fmt.Errorf("CRITICAL: %s token invalid JWT format (dots: %d)", tokenType, dotCount)
|
err := fmt.Errorf("%s token invalid JWT format (dots: %d)", tokenType, dotCount)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split into parts
|
// Split into parts
|
||||||
parts := strings.Split(token, ".")
|
parts := strings.Split(token, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
err := fmt.Errorf("CRITICAL: %s token invalid JWT structure", tokenType)
|
err := fmt.Errorf("%s token invalid JWT structure", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate each part is non-empty and contains valid base64url characters
|
// Validate each part is non-empty and contains valid base64url characters
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if part == "" {
|
if part == "" {
|
||||||
err := fmt.Errorf("CRITICAL: %s token has empty JWT part %d", tokenType, i)
|
err := fmt.Errorf("%s token has empty JWT part %d", tokenType, i)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +293,7 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
|
|||||||
(char >= 'a' && char <= 'z') ||
|
(char >= 'a' && char <= 'z') ||
|
||||||
(char >= '0' && char <= '9') ||
|
(char >= '0' && char <= '9') ||
|
||||||
char == '-' || char == '_' || char == '=') {
|
char == '-' || char == '_' || char == '=') {
|
||||||
err := fmt.Errorf("CRITICAL: %s token contains invalid base64url character in part %d", tokenType, i)
|
err := fmt.Errorf("%s token contains invalid base64url character in part %d", tokenType, i)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,15 +303,13 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
|
|||||||
// Padding can only be at the end
|
// Padding can only be at the end
|
||||||
paddingIndex := strings.Index(part, "=")
|
paddingIndex := strings.Index(part, "=")
|
||||||
if paddingIndex != len(part)-1 && paddingIndex != len(part)-2 {
|
if paddingIndex != len(part)-1 && paddingIndex != len(part)-2 {
|
||||||
err := fmt.Errorf("CRITICAL: %s token has invalid base64url padding in part %d", tokenType, i)
|
err := fmt.Errorf("%s token has invalid base64url padding in part %d", tokenType, i)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Check that after padding, no other characters exist
|
// Check that after padding, no other characters exist
|
||||||
for j := paddingIndex; j < len(part); j++ {
|
for j := paddingIndex; j < len(part); j++ {
|
||||||
if part[j] != '=' {
|
if part[j] != '=' {
|
||||||
err := fmt.Errorf("CRITICAL: %s token has characters after padding in part %d", tokenType, i)
|
err := fmt.Errorf("%s token has characters after padding in part %d", tokenType, i)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,18 +318,15 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
|
|||||||
|
|
||||||
// Additional length checks for JWT parts
|
// Additional length checks for JWT parts
|
||||||
if len(parts[0]) < 10 { // Header too short
|
if len(parts[0]) < 10 { // Header too short
|
||||||
err := fmt.Errorf("CRITICAL: %s token header too short", tokenType)
|
err := fmt.Errorf("%s token header too short", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(parts[1]) < 10 { // Payload too short
|
if len(parts[1]) < 10 { // Payload too short
|
||||||
err := fmt.Errorf("CRITICAL: %s token payload too short", tokenType)
|
err := fmt.Errorf("%s token payload too short", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(parts[2]) < 10 { // Signature too short
|
if len(parts[2]) < 10 { // Signature too short
|
||||||
err := fmt.Errorf("CRITICAL: %s token signature too short", tokenType)
|
err := fmt.Errorf("%s token signature too short", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,16 +337,14 @@ func (cm *ChunkManager) validateJWTFormat(token string, tokenType string) error
|
|||||||
func (cm *ChunkManager) validateOpaqueToken(token string, tokenType string) error {
|
func (cm *ChunkManager) validateOpaqueToken(token string, tokenType string) error {
|
||||||
// Check for obviously invalid characters for opaque tokens
|
// Check for obviously invalid characters for opaque tokens
|
||||||
if strings.Contains(token, " ") {
|
if strings.Contains(token, " ") {
|
||||||
err := fmt.Errorf("CRITICAL: %s opaque token contains spaces", tokenType)
|
err := fmt.Errorf("%s opaque token contains spaces", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for control characters
|
// Check for control characters
|
||||||
for _, char := range token {
|
for _, char := range token {
|
||||||
if char < 32 || char == 127 {
|
if char < 32 || char == 127 {
|
||||||
err := fmt.Errorf("CRITICAL: %s opaque token contains control characters", tokenType)
|
err := fmt.Errorf("%s opaque token contains control characters", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,8 +357,7 @@ func (cm *ChunkManager) validateOpaqueToken(token string, tokenType string) erro
|
|||||||
}
|
}
|
||||||
// Require at least 8 unique characters for reasonable entropy
|
// Require at least 8 unique characters for reasonable entropy
|
||||||
if len(uniqueChars) < 8 {
|
if len(uniqueChars) < 8 {
|
||||||
err := fmt.Errorf("CRITICAL: %s opaque token has insufficient entropy", tokenType)
|
err := fmt.Errorf("%s opaque token has insufficient entropy", tokenType)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,16 +371,14 @@ func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) erro
|
|||||||
|
|
||||||
// Basic length validation
|
// Basic length validation
|
||||||
if tokenLen < config.MinLength {
|
if tokenLen < config.MinLength {
|
||||||
err := fmt.Errorf("CRITICAL: %s token below minimum length (%d bytes, min: %d)",
|
err := fmt.Errorf("%s token below minimum length (%d bytes, min: %d)",
|
||||||
config.Type, tokenLen, config.MinLength)
|
config.Type, tokenLen, config.MinLength)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenLen > config.MaxLength {
|
if tokenLen > config.MaxLength {
|
||||||
err := fmt.Errorf("CRITICAL: %s token exceeds maximum length (%d bytes, max: %d)",
|
err := fmt.Errorf("%s token exceeds maximum length (%d bytes, max: %d)",
|
||||||
config.Type, tokenLen, config.MaxLength)
|
config.Type, tokenLen, config.MaxLength)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,20 +393,17 @@ func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) erro
|
|||||||
|
|
||||||
// Check for unreasonably large JWT parts (potential security issue)
|
// Check for unreasonably large JWT parts (potential security issue)
|
||||||
if headerLen > 5*1024 { // 5KB header limit
|
if headerLen > 5*1024 { // 5KB header limit
|
||||||
err := fmt.Errorf("CRITICAL: %s token header too large (%d bytes)", config.Type, headerLen)
|
err := fmt.Errorf("%s token header too large (%d bytes)", config.Type, headerLen)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if payloadLen > config.MaxLength-10*1024 { // Leave room for header and signature
|
if payloadLen > config.MaxLength-10*1024 { // Leave room for header and signature
|
||||||
err := fmt.Errorf("CRITICAL: %s token payload too large (%d bytes)", config.Type, payloadLen)
|
err := fmt.Errorf("%s token payload too large (%d bytes)", config.Type, payloadLen)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if signatureLen > 2*1024 { // 2KB signature limit
|
if signatureLen > 2*1024 { // 2KB signature limit
|
||||||
err := fmt.Errorf("CRITICAL: %s token signature too large (%d bytes)", config.Type, signatureLen)
|
err := fmt.Errorf("%s token signature too large (%d bytes)", config.Type, signatureLen)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,8 +413,7 @@ func (cm *ChunkManager) validateTokenSize(token string, config TokenConfig) erro
|
|||||||
if config.AllowOpaqueTokens && !strings.Contains(token, ".") {
|
if config.AllowOpaqueTokens && !strings.Contains(token, ".") {
|
||||||
// For opaque tokens, check for reasonable size limits
|
// For opaque tokens, check for reasonable size limits
|
||||||
if tokenLen > 8*1024 { // 8KB limit for opaque tokens
|
if tokenLen > 8*1024 { // 8KB limit for opaque tokens
|
||||||
err := fmt.Errorf("CRITICAL: %s opaque token unusually large (%d bytes)", config.Type, tokenLen)
|
err := fmt.Errorf("%s opaque token unusually large (%d bytes)", config.Type, tokenLen)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,21 +428,20 @@ func (cm *ChunkManager) validateChunkingEfficiency(token string, config TokenCon
|
|||||||
// If token is small enough to fit in a single chunk, warn about unnecessary chunking
|
// If token is small enough to fit in a single chunk, warn about unnecessary chunking
|
||||||
if tokenLen <= config.MaxChunkSize && tokenLen <= maxCookieSize {
|
if tokenLen <= config.MaxChunkSize && tokenLen <= maxCookieSize {
|
||||||
// This is just informational - not an error, but helps with monitoring
|
// This is just informational - not an error, but helps with monitoring
|
||||||
cm.logger.Debugf("INFO: %s token (%d bytes) could fit in single chunk", config.Type, tokenLen)
|
// Token could fit in single chunk - this is fine, just informational
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expected number of chunks
|
// Calculate expected number of chunks
|
||||||
expectedChunks := (tokenLen + config.MaxChunkSize - 1) / config.MaxChunkSize
|
expectedChunks := (tokenLen + config.MaxChunkSize - 1) / config.MaxChunkSize
|
||||||
if expectedChunks > config.MaxChunks {
|
if expectedChunks > config.MaxChunks {
|
||||||
err := fmt.Errorf("CRITICAL: %s token would require %d chunks (max: %d)",
|
err := fmt.Errorf("%s token would require %d chunks (max: %d)",
|
||||||
config.Type, expectedChunks, config.MaxChunks)
|
config.Type, expectedChunks, config.MaxChunks)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for potential storage efficiency issues
|
// Check for potential storage efficiency issues
|
||||||
if expectedChunks > 10 && tokenLen < 50*1024 {
|
if expectedChunks > 10 && tokenLen < 50*1024 {
|
||||||
cm.logger.Infof("WARNING: %s token requires many chunks (%d) for size (%d bytes) - consider token optimization",
|
cm.logger.Info("%s token requires many chunks (%d) for size (%d bytes) - consider token optimization",
|
||||||
config.Type, expectedChunks, tokenLen)
|
config.Type, expectedChunks, tokenLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,15 +476,13 @@ func (cm *ChunkManager) validateTokenContent(token string, config TokenConfig) e
|
|||||||
func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConfig) error {
|
func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConfig) error {
|
||||||
// Check for null bytes (potential injection attacks)
|
// Check for null bytes (potential injection attacks)
|
||||||
if strings.Contains(token, "\x00") {
|
if strings.Contains(token, "\x00") {
|
||||||
err := fmt.Errorf("CRITICAL: %s token contains null bytes", config.Type)
|
err := fmt.Errorf("%s token contains null bytes", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for line feed/carriage return (header injection attacks)
|
// Check for line feed/carriage return (header injection attacks)
|
||||||
if strings.ContainsAny(token, "\r\n") {
|
if strings.ContainsAny(token, "\r\n") {
|
||||||
err := fmt.Errorf("CRITICAL: %s token contains line breaks", config.Type)
|
err := fmt.Errorf("%s token contains line breaks", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,8 +496,7 @@ func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConf
|
|||||||
tokenLower := strings.ToLower(token)
|
tokenLower := strings.ToLower(token)
|
||||||
for _, pattern := range suspiciousPatterns {
|
for _, pattern := range suspiciousPatterns {
|
||||||
if strings.Contains(tokenLower, pattern) {
|
if strings.Contains(tokenLower, pattern) {
|
||||||
err := fmt.Errorf("CRITICAL: %s token contains suspicious pattern: %s", config.Type, pattern)
|
err := fmt.Errorf("%s token contains suspicious pattern: %s", config.Type, pattern)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,8 +513,7 @@ func (cm *ChunkManager) validateTokenSanitization(token string, config TokenConf
|
|||||||
func (cm *ChunkManager) validateJWTContent(token string, config TokenConfig) error {
|
func (cm *ChunkManager) validateJWTContent(token string, config TokenConfig) error {
|
||||||
parts := strings.Split(token, ".")
|
parts := strings.Split(token, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT token malformed for content validation", config.Type)
|
err := fmt.Errorf("%s JWT token malformed for content validation", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,15 +539,13 @@ func (cm *ChunkManager) validateJWTContent(token string, config TokenConfig) err
|
|||||||
func (cm *ChunkManager) validateJWTHeader(header string, config TokenConfig) error {
|
func (cm *ChunkManager) validateJWTHeader(header string, config TokenConfig) error {
|
||||||
// Basic header structure validation
|
// Basic header structure validation
|
||||||
if len(header) == 0 {
|
if len(header) == 0 {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT header is empty", config.Type)
|
err := fmt.Errorf("%s JWT header is empty", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate base64url encoding
|
// Validate base64url encoding
|
||||||
if _, err := base64.RawURLEncoding.DecodeString(header); err != nil {
|
if _, err := base64.RawURLEncoding.DecodeString(header); err != nil {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT header not valid base64url", config.Type)
|
err := fmt.Errorf("%s JWT header not valid base64url", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,15 +556,13 @@ func (cm *ChunkManager) validateJWTHeader(header string, config TokenConfig) err
|
|||||||
func (cm *ChunkManager) validateJWTPayload(payload string, config TokenConfig) error {
|
func (cm *ChunkManager) validateJWTPayload(payload string, config TokenConfig) error {
|
||||||
// Basic payload structure validation
|
// Basic payload structure validation
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT payload is empty", config.Type)
|
err := fmt.Errorf("%s JWT payload is empty", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload should be decodable (basic structural check)
|
// Payload should be decodable (basic structural check)
|
||||||
if _, err := base64.RawURLEncoding.DecodeString(payload); err != nil {
|
if _, err := base64.RawURLEncoding.DecodeString(payload); err != nil {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT payload not valid base64url", config.Type)
|
err := fmt.Errorf("%s JWT payload not valid base64url", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,15 +573,13 @@ func (cm *ChunkManager) validateJWTPayload(payload string, config TokenConfig) e
|
|||||||
func (cm *ChunkManager) validateJWTSignature(signature string, config TokenConfig) error {
|
func (cm *ChunkManager) validateJWTSignature(signature string, config TokenConfig) error {
|
||||||
// Basic signature structure validation
|
// Basic signature structure validation
|
||||||
if len(signature) == 0 {
|
if len(signature) == 0 {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT signature is empty", config.Type)
|
err := fmt.Errorf("%s JWT signature is empty", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate base64url encoding
|
// Validate base64url encoding
|
||||||
if _, err := base64.RawURLEncoding.DecodeString(signature); err != nil {
|
if _, err := base64.RawURLEncoding.DecodeString(signature); err != nil {
|
||||||
err := fmt.Errorf("CRITICAL: %s JWT signature not valid base64url", config.Type)
|
err := fmt.Errorf("%s JWT signature not valid base64url", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,8 +612,7 @@ func (cm *ChunkManager) validateOpaqueTokenContent(token string, config TokenCon
|
|||||||
|
|
||||||
// Opaque tokens should have reasonable character distribution
|
// Opaque tokens should have reasonable character distribution
|
||||||
if alphaRatio < 0.1 && numericRatio < 0.1 {
|
if alphaRatio < 0.1 && numericRatio < 0.1 {
|
||||||
err := fmt.Errorf("CRITICAL: %s opaque token has suspicious character distribution", config.Type)
|
err := fmt.Errorf("%s opaque token has suspicious character distribution", config.Type)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,7 +635,7 @@ func (cm *ChunkManager) validateOpaqueTokenContent(token string, config TokenCon
|
|||||||
|
|
||||||
// For longer tokens without legitimate prefixes, be more suspicious
|
// For longer tokens without legitimate prefixes, be more suspicious
|
||||||
if len(token) > 50 && !hasLegitimatePrefix {
|
if len(token) > 50 && !hasLegitimatePrefix {
|
||||||
cm.logger.Debugf("WARNING: %s opaque token lacks common token prefixes", config.Type)
|
// Opaque token without common prefixes - this is fine
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -697,9 +667,8 @@ func (cm *ChunkManager) detectRepeatedCharacters(token string, config TokenConfi
|
|||||||
// Flag tokens with excessive character repetition
|
// Flag tokens with excessive character repetition
|
||||||
threshold := 20 // Allow up to 20 consecutive identical characters
|
threshold := 20 // Allow up to 20 consecutive identical characters
|
||||||
if maxRepeated > threshold {
|
if maxRepeated > threshold {
|
||||||
err := fmt.Errorf("CRITICAL: %s token has excessive repeated characters (%d consecutive)",
|
err := fmt.Errorf("%s token has excessive repeated characters (%d consecutive)",
|
||||||
config.Type, maxRepeated)
|
config.Type, maxRepeated)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,9 +684,8 @@ func (cm *ChunkManager) detectRepeatedCharacters(token string, config TokenConfi
|
|||||||
|
|
||||||
// Flag if any single character makes up more than 70% of the token
|
// Flag if any single character makes up more than 70% of the token
|
||||||
if frequency > 0.7 && tokenLen > 20 {
|
if frequency > 0.7 && tokenLen > 20 {
|
||||||
err := fmt.Errorf("CRITICAL: %s token has suspicious character frequency (char '%c': %.1f%%)",
|
err := fmt.Errorf("%s token has suspicious character frequency (char '%c': %.1f%%)",
|
||||||
config.Type, char, frequency*100)
|
config.Type, char, frequency*100)
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,8 +710,7 @@ func (cm *ChunkManager) validateTokenExpiration(token string, config TokenConfig
|
|||||||
|
|
||||||
// Check if token is expired
|
// Check if token is expired
|
||||||
if expiration != nil && time.Now().After(*expiration) {
|
if expiration != nil && time.Now().After(*expiration) {
|
||||||
err := fmt.Errorf("CRITICAL: %s token is expired (expired at: %v)", config.Type, expiration.Format(time.RFC3339))
|
err := fmt.Errorf("%s token is expired (expired at: %v)", config.Type, expiration.Format(time.RFC3339))
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +718,7 @@ func (cm *ChunkManager) validateTokenExpiration(token string, config TokenConfig
|
|||||||
if expiration != nil {
|
if expiration != nil {
|
||||||
maxFutureTime := time.Now().Add(10 * 365 * 24 * time.Hour) // 10 years
|
maxFutureTime := time.Now().Add(10 * 365 * 24 * time.Hour) // 10 years
|
||||||
if expiration.After(maxFutureTime) {
|
if expiration.After(maxFutureTime) {
|
||||||
cm.logger.Infof("WARNING: %s token expires very far in future (%v) - potential security issue",
|
cm.logger.Info("%s token expires very far in future (%v) - potential security issue",
|
||||||
config.Type, expiration.Format(time.RFC3339))
|
config.Type, expiration.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -819,16 +786,15 @@ func (cm *ChunkManager) validateTokenFreshness(token string, config TokenConfig)
|
|||||||
|
|
||||||
// Check if token was issued in the future (clock skew tolerance: 5 minutes)
|
// Check if token was issued in the future (clock skew tolerance: 5 minutes)
|
||||||
if issuedAt.After(now.Add(5 * time.Minute)) {
|
if issuedAt.After(now.Add(5 * time.Minute)) {
|
||||||
err := fmt.Errorf("CRITICAL: %s token issued in future (issued at: %v)",
|
err := fmt.Errorf("%s token issued in future (issued at: %v)",
|
||||||
config.Type, issuedAt.Format(time.RFC3339))
|
config.Type, issuedAt.Format(time.RFC3339))
|
||||||
cm.logger.Error(err.Error())
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token is too old (potential replay attack)
|
// Check if token is too old (potential replay attack)
|
||||||
maxAge := 24 * time.Hour // Tokens older than 24 hours are suspicious
|
maxAge := 24 * time.Hour // Tokens older than 24 hours are suspicious
|
||||||
if now.Sub(*issuedAt) > maxAge {
|
if now.Sub(*issuedAt) > maxAge {
|
||||||
cm.logger.Infof("WARNING: %s token is quite old (issued: %v) - potential replay",
|
cm.logger.Info("%s token is quite old (issued: %v) - potential replay",
|
||||||
config.Type, issuedAt.Format(time.RFC3339))
|
config.Type, issuedAt.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user