Files
traefikoidc/goroutine_manager.go
T
lukaszraczylo 1b49e133da Complete rebuild of the plugin
* 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.
2025-09-18 11:01:30 +01:00

166 lines
4.0 KiB
Go

package traefikoidc
import (
"context"
"sync"
"time"
)
// GoroutineManager manages background goroutines with proper lifecycle
type GoroutineManager struct {
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
goroutines map[string]*managedGoroutine
logger *Logger
}
type managedGoroutine struct {
name string
cancel context.CancelFunc
startTime time.Time
running bool
}
// NewGoroutineManager creates a new goroutine manager
func NewGoroutineManager(logger *Logger) *GoroutineManager {
ctx, cancel := context.WithCancel(context.Background())
return &GoroutineManager{
ctx: ctx,
cancel: cancel,
goroutines: make(map[string]*managedGoroutine),
logger: logger,
}
}
// StartGoroutine starts a managed goroutine with context-based cancellation
func (m *GoroutineManager) StartGoroutine(name string, fn func(context.Context)) {
m.mu.Lock()
defer m.mu.Unlock()
// Check if goroutine with this name already exists
if existing, exists := m.goroutines[name]; exists && existing.running {
m.logger.Debugf("Goroutine %s already running, skipping start", name)
return
}
// Create goroutine-specific context
goroutineCtx, goroutineCancel := context.WithCancel(m.ctx)
managed := &managedGoroutine{
name: name,
cancel: goroutineCancel,
startTime: time.Now(),
running: true,
}
m.goroutines[name] = managed
m.wg.Add(1)
go func(managedGoroutine *managedGoroutine, goroutineName string) {
defer func() {
m.wg.Done()
m.mu.Lock()
managedGoroutine.running = false
m.mu.Unlock()
// Recover from panics
if r := recover(); r != nil {
m.logger.Errorf("Goroutine %s panic recovered: %v", goroutineName, r)
}
}()
m.logger.Debugf("Starting goroutine: %s", goroutineName)
fn(goroutineCtx)
m.logger.Debugf("Goroutine %s finished", goroutineName)
}(managed, name)
}
// StartPeriodicTask starts a periodic task with context-based cancellation
func (m *GoroutineManager) StartPeriodicTask(name string, interval time.Duration, task func()) {
m.StartGoroutine(name, func(ctx context.Context) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
m.logger.Debugf("Periodic task %s cancelled", name)
return
case <-ticker.C:
task()
}
}
})
}
// StopGoroutine stops a specific goroutine by name
func (m *GoroutineManager) StopGoroutine(name string) {
m.mu.Lock()
defer m.mu.Unlock()
if managed, exists := m.goroutines[name]; exists && managed.running {
m.logger.Debugf("Stopping goroutine: %s", name)
managed.cancel()
}
}
// Shutdown gracefully shuts down all managed goroutines
func (m *GoroutineManager) Shutdown(timeout time.Duration) error {
m.logger.Debug("Starting goroutine manager shutdown")
// Cancel the main context to signal all goroutines to stop
m.cancel()
// Wait for all goroutines with timeout
done := make(chan struct{})
go func() {
m.wg.Wait()
close(done)
}()
select {
case <-done:
m.logger.Debug("All goroutines stopped gracefully")
return nil
case <-time.After(timeout):
m.logger.Error("Timeout waiting for goroutines to stop")
return ErrShutdownTimeout
}
}
// GetStatus returns the status of all managed goroutines
func (m *GoroutineManager) GetStatus() map[string]GoroutineStatus {
m.mu.RLock()
defer m.mu.RUnlock()
status := make(map[string]GoroutineStatus)
for name, managed := range m.goroutines {
status[name] = GoroutineStatus{
Name: managed.name,
Running: managed.running,
StartTime: managed.startTime,
Runtime: time.Since(managed.startTime),
}
}
return status
}
// GoroutineStatus represents the status of a managed goroutine
type GoroutineStatus struct {
Name string
Running bool
StartTime time.Time
Runtime time.Duration
}
// ErrShutdownTimeout is returned when shutdown times out
var ErrShutdownTimeout = &shutdownTimeoutError{}
type shutdownTimeoutError struct{}
func (e *shutdownTimeoutError) Error() string {
return "shutdown timeout: some goroutines did not stop in time"
}