mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +00:00
1b49e133da
* Fix bug affecting Azure OIDC authentication ( and most likely others ) * Fixes issue #51 * Ensure that appended roles are unique. Update the documentation. * Improvements targetting possible memory usage spikes. * Additional fixes and cleanup * Refactoring code to fix the issues identified by the users. * Modernize run * Fieldalignment * Multiple changes to improve performance and reduce complexity. - Optimise the errors and recovery. - Deduplicate code in metadata cache. - Remove unused performance monitoring code. - Simplify session management and settings handling. * Fix claims issue. * Add ability to overwrite the default scopes in the settings file * Well.. that escalated quickly. Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ). * Bugfix #51: Ensures that user provided scopes overrides work. * fixup! Bugfix #51: Ensures that user provided scopes overrides work. * fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work. * Abstract the provider logic into a separate package. * Additional micro fixes and cleanups. * Simplify all the things. * fixup! Simplify all the things. * fixup! fixup! Simplify all the things. * fixup! fixup! fixup! Simplify all the things. * fixup! fixup! fixup! fixup! Simplify all the things. * ... * Cleanup tests. * fixup! Cleanup tests. * fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! Cleanup tests. * fixup! fixup! fixup! fixup! fixup! Cleanup tests. * Issue #53: Fix CSRF token handling in reverse proxy 1. ✅ HTTPS Detection Fixed (session.go:723) - Now uses X-Forwarded-Proto header instead of r.URL.Scheme - Properly detects HTTPS in reverse proxy environments 2. ✅ SameSite Cookie Attribute Fixed - Removed automatic SameSiteStrictMode for HTTPS (would break OAuth) - Keeps SameSiteLaxMode to allow OAuth callbacks from external domains - Only uses Strict for AJAX requests which don't involve OAuth redirects 3. ✅ Cookie Domain Handling Fixed - Now respects X-Forwarded-Host header for cookie domain - Ensures cookies are set for the public domain, not internal proxy domain 4. ✅ EnhanceSessionSecurity Properly Integrated - Function is now actually called during session save - Applies security enhancements without breaking OAuth flow Why Issue #53 Failed Before: 1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back) 2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail 3. Cookie domain might have been wrong (internal vs public domain) Why It Works Now: 1. Cookies are properly marked Secure for HTTPS 2. Uses SameSite=Lax to allow OAuth provider callbacks 3. Cookie domain uses public domain from X-Forwarded-Host 4. CSRF token persists through the entire OAuth flow * Next set of enhancements together with memory usage improvements. * Memory leak fixes and optimisations. * CSRF and Cookie Domain fixes * fixup! CSRF and Cookie Domain fixes * Metadata cache leak fix + profiling * fixup! Metadata cache leak fix + profiling * Memory leaks hunting, part 1337. * Further pursue of perfection. * fixup! Further pursue of perfection. * fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection. * Clear race conditions * fixup! Clear race conditions * Weekend fun with memory leaks * Splitting code into multiple files with reasonable testing coverage. ``` ok github.com/lukaszraczylo/traefikoidc 117.017s coverage: 72.6% of statements ok github.com/lukaszraczylo/traefikoidc/auth 0.505s coverage: 87.1% of statements ok github.com/lukaszraczylo/traefikoidc/circuit_breaker 0.283s coverage: 99.0% of statements github.com/lukaszraczylo/traefikoidc/config coverage: 0.0% of statements ok github.com/lukaszraczylo/traefikoidc/handlers 0.349s coverage: 98.2% of statements ok github.com/lukaszraczylo/traefikoidc/internal/providers (cached) coverage: 94.3% of statements ok github.com/lukaszraczylo/traefikoidc/middleware 0.808s coverage: 78.0% of statements ok github.com/lukaszraczylo/traefikoidc/recovery 0.653s coverage: 100.0% of statements ok github.com/lukaszraczylo/traefikoidc/session/chunking (cached) coverage: 87.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/core (cached) coverage: 85.6% of statements ok github.com/lukaszraczylo/traefikoidc/session/crypto (cached) coverage: 81.8% of statements ok github.com/lukaszraczylo/traefikoidc/session/storage (cached) coverage: 93.5% of statements ok github.com/lukaszraczylo/traefikoidc/session/validators (cached) coverage: 98.8% of statements ```` * fixup! Splitting code into multiple files with reasonable testing coverage. * fixup! fixup! Splitting code into multiple files with reasonable testing coverage. * Weekend fun with further optimisations. * fixup! Weekend fun with further optimisations. * fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations. * Pre-release cleanup. * Enhance test coverage. * fixup! Enhance test coverage. * fixup! fixup! Enhance test coverage. * fixup! fixup! fixup! Enhance test coverage.
474 lines
11 KiB
Go
474 lines
11 KiB
Go
// Package pool provides a unified, centralized memory pool management system
|
|
// for the entire application. It consolidates all duplicate pool implementations
|
|
// into a single, efficient, and thread-safe package.
|
|
package pool
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// Manager is the centralized pool manager that consolidates all memory pools
|
|
// used throughout the application. It provides a single entry point for
|
|
// all pooling operations, reducing duplicate code and improving maintainability.
|
|
type Manager struct {
|
|
// Buffer pools
|
|
smallBufferPool *sync.Pool // 1KB buffers
|
|
mediumBufferPool *sync.Pool // 4KB buffers
|
|
largeBufferPool *sync.Pool // 8KB buffers
|
|
xlBufferPool *sync.Pool // 16KB buffers
|
|
|
|
// Compression pools
|
|
gzipWriterPool *sync.Pool
|
|
gzipReaderPool *sync.Pool
|
|
|
|
// String builder pool
|
|
stringBuilderPool *sync.Pool
|
|
|
|
// JWT parsing buffers
|
|
jwtBufferPool *sync.Pool
|
|
|
|
// HTTP response buffers
|
|
httpResponsePool *sync.Pool
|
|
|
|
// Byte slice pools for various sizes
|
|
byteSlicePools map[int]*sync.Pool
|
|
poolMu sync.RWMutex
|
|
|
|
// Statistics
|
|
stats PoolStats
|
|
}
|
|
|
|
// PoolStats tracks pool usage statistics
|
|
type PoolStats struct {
|
|
BufferGets uint64
|
|
BufferPuts uint64
|
|
GzipGets uint64
|
|
GzipPuts uint64
|
|
StringGets uint64
|
|
StringPuts uint64
|
|
JWTGets uint64
|
|
JWTPuts uint64
|
|
HTTPGets uint64
|
|
HTTPPuts uint64
|
|
OversizedRejects uint64
|
|
}
|
|
|
|
// JWTBuffer provides pre-allocated buffers for JWT parsing
|
|
type JWTBuffer struct {
|
|
Header []byte
|
|
Payload []byte
|
|
Signature []byte
|
|
}
|
|
|
|
var (
|
|
// globalManager is the singleton pool manager instance
|
|
globalManager *Manager
|
|
// managerOnce ensures single initialization
|
|
managerOnce sync.Once
|
|
)
|
|
|
|
// Get returns the global pool manager instance
|
|
func Get() *Manager {
|
|
managerOnce.Do(func() {
|
|
globalManager = newManager()
|
|
})
|
|
return globalManager
|
|
}
|
|
|
|
// newManager creates a new pool manager with all pools initialized
|
|
func newManager() *Manager {
|
|
m := &Manager{
|
|
byteSlicePools: make(map[int]*sync.Pool),
|
|
}
|
|
|
|
// Initialize buffer pools with different sizes
|
|
m.smallBufferPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return bytes.NewBuffer(make([]byte, 0, 1024))
|
|
},
|
|
}
|
|
|
|
m.mediumBufferPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return bytes.NewBuffer(make([]byte, 0, 4096))
|
|
},
|
|
}
|
|
|
|
m.largeBufferPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return bytes.NewBuffer(make([]byte, 0, 8192))
|
|
},
|
|
}
|
|
|
|
m.xlBufferPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return bytes.NewBuffer(make([]byte, 0, 16384))
|
|
},
|
|
}
|
|
|
|
// Initialize compression pools
|
|
m.gzipWriterPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
w, _ := gzip.NewWriterLevel(nil, gzip.BestSpeed)
|
|
return w
|
|
},
|
|
}
|
|
|
|
m.gzipReaderPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return (*gzip.Reader)(nil)
|
|
},
|
|
}
|
|
|
|
// Initialize string builder pool
|
|
m.stringBuilderPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
sb := &strings.Builder{}
|
|
sb.Grow(1024)
|
|
return sb
|
|
},
|
|
}
|
|
|
|
// Initialize JWT buffer pool
|
|
m.jwtBufferPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return &JWTBuffer{
|
|
Header: make([]byte, 0, 512),
|
|
Payload: make([]byte, 0, 2048),
|
|
Signature: make([]byte, 0, 512),
|
|
}
|
|
},
|
|
}
|
|
|
|
// Initialize HTTP response buffer pool
|
|
m.httpResponsePool = &sync.Pool{
|
|
New: func() interface{} {
|
|
buf := make([]byte, 0, 8192)
|
|
return &buf
|
|
},
|
|
}
|
|
|
|
// Initialize common byte slice pools
|
|
for _, size := range []int{256, 512, 1024, 2048, 4096, 8192, 16384} {
|
|
size := size // capture for closure
|
|
m.byteSlicePools[size] = &sync.Pool{
|
|
New: func() interface{} {
|
|
b := make([]byte, size)
|
|
return &b
|
|
},
|
|
}
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// GetBuffer returns a buffer from the appropriate pool based on size hint
|
|
func (m *Manager) GetBuffer(sizeHint int) *bytes.Buffer {
|
|
atomic.AddUint64(&m.stats.BufferGets, 1)
|
|
|
|
switch {
|
|
case sizeHint <= 1024:
|
|
return m.smallBufferPool.Get().(*bytes.Buffer)
|
|
case sizeHint <= 4096:
|
|
return m.mediumBufferPool.Get().(*bytes.Buffer)
|
|
case sizeHint <= 8192:
|
|
return m.largeBufferPool.Get().(*bytes.Buffer)
|
|
case sizeHint <= 16384:
|
|
return m.xlBufferPool.Get().(*bytes.Buffer)
|
|
default:
|
|
// For very large buffers, create new ones
|
|
return bytes.NewBuffer(make([]byte, 0, sizeHint))
|
|
}
|
|
}
|
|
|
|
// PutBuffer returns a buffer to the appropriate pool
|
|
func (m *Manager) PutBuffer(buf *bytes.Buffer) {
|
|
if buf == nil {
|
|
return
|
|
}
|
|
|
|
atomic.AddUint64(&m.stats.BufferPuts, 1)
|
|
|
|
// Reset buffer before returning to pool
|
|
capacity := buf.Cap()
|
|
buf.Reset()
|
|
|
|
// Reject oversized buffers to prevent memory bloat
|
|
if capacity > 32768 {
|
|
atomic.AddUint64(&m.stats.OversizedRejects, 1)
|
|
return
|
|
}
|
|
|
|
// Return to appropriate pool based on capacity
|
|
switch {
|
|
case capacity <= 1024:
|
|
m.smallBufferPool.Put(buf)
|
|
case capacity <= 4096:
|
|
m.mediumBufferPool.Put(buf)
|
|
case capacity <= 8192:
|
|
m.largeBufferPool.Put(buf)
|
|
case capacity <= 16384:
|
|
m.xlBufferPool.Put(buf)
|
|
}
|
|
}
|
|
|
|
// GetGzipWriter returns a gzip writer from the pool
|
|
func (m *Manager) GetGzipWriter() *gzip.Writer {
|
|
atomic.AddUint64(&m.stats.GzipGets, 1)
|
|
return m.gzipWriterPool.Get().(*gzip.Writer)
|
|
}
|
|
|
|
// PutGzipWriter returns a gzip writer to the pool
|
|
func (m *Manager) PutGzipWriter(w *gzip.Writer) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
atomic.AddUint64(&m.stats.GzipPuts, 1)
|
|
w.Reset(nil)
|
|
m.gzipWriterPool.Put(w)
|
|
}
|
|
|
|
// GetGzipReader returns a gzip reader from the pool
|
|
func (m *Manager) GetGzipReader() *gzip.Reader {
|
|
atomic.AddUint64(&m.stats.GzipGets, 1)
|
|
r := m.gzipReaderPool.Get()
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.(*gzip.Reader)
|
|
}
|
|
|
|
// PutGzipReader returns a gzip reader to the pool
|
|
func (m *Manager) PutGzipReader(r *gzip.Reader) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
atomic.AddUint64(&m.stats.GzipPuts, 1)
|
|
r.Reset(nil)
|
|
m.gzipReaderPool.Put(r)
|
|
}
|
|
|
|
// GetStringBuilder returns a string builder from the pool
|
|
func (m *Manager) GetStringBuilder() *strings.Builder {
|
|
atomic.AddUint64(&m.stats.StringGets, 1)
|
|
sb := m.stringBuilderPool.Get().(*strings.Builder)
|
|
sb.Reset()
|
|
return sb
|
|
}
|
|
|
|
// PutStringBuilder returns a string builder to the pool
|
|
func (m *Manager) PutStringBuilder(sb *strings.Builder) {
|
|
if sb == nil {
|
|
return
|
|
}
|
|
|
|
atomic.AddUint64(&m.stats.StringPuts, 1)
|
|
|
|
// Reject oversized builders
|
|
if sb.Cap() > 16384 {
|
|
atomic.AddUint64(&m.stats.OversizedRejects, 1)
|
|
return
|
|
}
|
|
|
|
sb.Reset()
|
|
m.stringBuilderPool.Put(sb)
|
|
}
|
|
|
|
// GetJWTBuffer returns JWT parsing buffers from the pool
|
|
func (m *Manager) GetJWTBuffer() *JWTBuffer {
|
|
atomic.AddUint64(&m.stats.JWTGets, 1)
|
|
return m.jwtBufferPool.Get().(*JWTBuffer)
|
|
}
|
|
|
|
// PutJWTBuffer returns JWT parsing buffers to the pool
|
|
func (m *Manager) PutJWTBuffer(buf *JWTBuffer) {
|
|
if buf == nil {
|
|
return
|
|
}
|
|
|
|
atomic.AddUint64(&m.stats.JWTPuts, 1)
|
|
|
|
// Check for oversized buffers
|
|
if cap(buf.Header) > 2048 || cap(buf.Payload) > 8192 || cap(buf.Signature) > 2048 {
|
|
atomic.AddUint64(&m.stats.OversizedRejects, 1)
|
|
return
|
|
}
|
|
|
|
// Reset slices to zero length
|
|
buf.Header = buf.Header[:0]
|
|
buf.Payload = buf.Payload[:0]
|
|
buf.Signature = buf.Signature[:0]
|
|
m.jwtBufferPool.Put(buf)
|
|
}
|
|
|
|
// GetHTTPResponseBuffer returns an HTTP response buffer from the pool
|
|
func (m *Manager) GetHTTPResponseBuffer() []byte {
|
|
atomic.AddUint64(&m.stats.HTTPGets, 1)
|
|
return *m.httpResponsePool.Get().(*[]byte)
|
|
}
|
|
|
|
// PutHTTPResponseBuffer returns an HTTP response buffer to the pool
|
|
func (m *Manager) PutHTTPResponseBuffer(buf []byte) {
|
|
if buf == nil {
|
|
return
|
|
}
|
|
|
|
atomic.AddUint64(&m.stats.HTTPPuts, 1)
|
|
|
|
// Reject oversized buffers
|
|
if cap(buf) > 32768 {
|
|
atomic.AddUint64(&m.stats.OversizedRejects, 1)
|
|
return
|
|
}
|
|
|
|
buf = buf[:0]
|
|
m.httpResponsePool.Put(&buf)
|
|
}
|
|
|
|
// GetByteSlice returns a byte slice of the specified size from the pool
|
|
func (m *Manager) GetByteSlice(size int) []byte {
|
|
m.poolMu.RLock()
|
|
pool, exists := m.byteSlicePools[size]
|
|
m.poolMu.RUnlock()
|
|
|
|
if !exists {
|
|
// Round up to nearest power of 2
|
|
poolSize := 1
|
|
for poolSize < size {
|
|
poolSize *= 2
|
|
}
|
|
|
|
m.poolMu.Lock()
|
|
// Double-check after acquiring write lock
|
|
pool, exists = m.byteSlicePools[poolSize]
|
|
if !exists {
|
|
pool = &sync.Pool{
|
|
New: func() interface{} {
|
|
b := make([]byte, poolSize)
|
|
return &b
|
|
},
|
|
}
|
|
m.byteSlicePools[poolSize] = pool
|
|
}
|
|
m.poolMu.Unlock()
|
|
}
|
|
|
|
b := pool.Get().(*[]byte)
|
|
return (*b)[:size]
|
|
}
|
|
|
|
// PutByteSlice returns a byte slice to the pool
|
|
func (m *Manager) PutByteSlice(b []byte) {
|
|
if b == nil || cap(b) > 65536 { // Don't pool very large slices
|
|
return
|
|
}
|
|
|
|
size := cap(b)
|
|
m.poolMu.RLock()
|
|
pool, exists := m.byteSlicePools[size]
|
|
m.poolMu.RUnlock()
|
|
|
|
if exists {
|
|
b = b[:0]
|
|
pool.Put(&b)
|
|
}
|
|
}
|
|
|
|
// GetStats returns current pool statistics
|
|
func (m *Manager) GetStats() PoolStats {
|
|
return PoolStats{
|
|
BufferGets: atomic.LoadUint64(&m.stats.BufferGets),
|
|
BufferPuts: atomic.LoadUint64(&m.stats.BufferPuts),
|
|
GzipGets: atomic.LoadUint64(&m.stats.GzipGets),
|
|
GzipPuts: atomic.LoadUint64(&m.stats.GzipPuts),
|
|
StringGets: atomic.LoadUint64(&m.stats.StringGets),
|
|
StringPuts: atomic.LoadUint64(&m.stats.StringPuts),
|
|
JWTGets: atomic.LoadUint64(&m.stats.JWTGets),
|
|
JWTPuts: atomic.LoadUint64(&m.stats.JWTPuts),
|
|
HTTPGets: atomic.LoadUint64(&m.stats.HTTPGets),
|
|
HTTPPuts: atomic.LoadUint64(&m.stats.HTTPPuts),
|
|
OversizedRejects: atomic.LoadUint64(&m.stats.OversizedRejects),
|
|
}
|
|
}
|
|
|
|
// ResetStats resets all statistics counters
|
|
func (m *Manager) ResetStats() {
|
|
atomic.StoreUint64(&m.stats.BufferGets, 0)
|
|
atomic.StoreUint64(&m.stats.BufferPuts, 0)
|
|
atomic.StoreUint64(&m.stats.GzipGets, 0)
|
|
atomic.StoreUint64(&m.stats.GzipPuts, 0)
|
|
atomic.StoreUint64(&m.stats.StringGets, 0)
|
|
atomic.StoreUint64(&m.stats.StringPuts, 0)
|
|
atomic.StoreUint64(&m.stats.JWTGets, 0)
|
|
atomic.StoreUint64(&m.stats.JWTPuts, 0)
|
|
atomic.StoreUint64(&m.stats.HTTPGets, 0)
|
|
atomic.StoreUint64(&m.stats.HTTPPuts, 0)
|
|
atomic.StoreUint64(&m.stats.OversizedRejects, 0)
|
|
}
|
|
|
|
// Global convenience functions
|
|
|
|
// Buffer returns a buffer from the global pool
|
|
func Buffer(sizeHint int) *bytes.Buffer {
|
|
return Get().GetBuffer(sizeHint)
|
|
}
|
|
|
|
// ReturnBuffer returns a buffer to the global pool
|
|
func ReturnBuffer(buf *bytes.Buffer) {
|
|
Get().PutBuffer(buf)
|
|
}
|
|
|
|
// GzipWriter returns a gzip writer from the global pool
|
|
func GzipWriter() *gzip.Writer {
|
|
return Get().GetGzipWriter()
|
|
}
|
|
|
|
// ReturnGzipWriter returns a gzip writer to the global pool
|
|
func ReturnGzipWriter(w *gzip.Writer) {
|
|
Get().PutGzipWriter(w)
|
|
}
|
|
|
|
// StringBuilder returns a string builder from the global pool
|
|
func StringBuilder() *strings.Builder {
|
|
return Get().GetStringBuilder()
|
|
}
|
|
|
|
// ReturnStringBuilder returns a string builder to the global pool
|
|
func ReturnStringBuilder(sb *strings.Builder) {
|
|
Get().PutStringBuilder(sb)
|
|
}
|
|
|
|
// JWTBuffers returns JWT parsing buffers from the global pool
|
|
func JWTBuffers() *JWTBuffer {
|
|
return Get().GetJWTBuffer()
|
|
}
|
|
|
|
// ReturnJWTBuffers returns JWT parsing buffers to the global pool
|
|
func ReturnJWTBuffers(buf *JWTBuffer) {
|
|
Get().PutJWTBuffer(buf)
|
|
}
|
|
|
|
// HTTPBuffer returns an HTTP response buffer from the global pool
|
|
func HTTPBuffer() []byte {
|
|
return Get().GetHTTPResponseBuffer()
|
|
}
|
|
|
|
// ReturnHTTPBuffer returns an HTTP response buffer to the global pool
|
|
func ReturnHTTPBuffer(buf []byte) {
|
|
Get().PutHTTPResponseBuffer(buf)
|
|
}
|
|
|
|
// ByteSlice returns a byte slice from the global pool
|
|
func ByteSlice(size int) []byte {
|
|
return Get().GetByteSlice(size)
|
|
}
|
|
|
|
// ReturnByteSlice returns a byte slice to the global pool
|
|
func ReturnByteSlice(b []byte) {
|
|
Get().PutByteSlice(b)
|
|
}
|