mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
fixup! Cleanup.
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
// Package patterns provides cached compiled regex patterns for performance optimization
|
||||
package patterns
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// RegexCache manages compiled regex patterns with thread-safe access
|
||||
type RegexCache struct {
|
||||
patterns map[string]*regexp.Regexp
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRegexCache creates a new regex cache instance
|
||||
func NewRegexCache() *RegexCache {
|
||||
return &RegexCache{
|
||||
patterns: make(map[string]*regexp.Regexp),
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a compiled regex pattern, compiling and caching it if not present
|
||||
func (c *RegexCache) Get(pattern string) (*regexp.Regexp, error) {
|
||||
// First try read lock for existing pattern
|
||||
c.mu.RLock()
|
||||
if regex, exists := c.patterns[pattern]; exists {
|
||||
c.mu.RUnlock()
|
||||
return regex, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Pattern not found, acquire write lock to compile and cache
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Double-check in case another goroutine compiled it while we waited
|
||||
if regex, exists := c.patterns[pattern]; exists {
|
||||
return regex, nil
|
||||
}
|
||||
|
||||
// Compile the pattern
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the compiled pattern
|
||||
c.patterns[pattern] = regex
|
||||
return regex, nil
|
||||
}
|
||||
|
||||
// MustGet is like Get but panics if the pattern cannot be compiled
|
||||
func (c *RegexCache) MustGet(pattern string) *regexp.Regexp {
|
||||
regex, err := c.Get(pattern)
|
||||
if err != nil {
|
||||
panic("regex compilation failed for pattern '" + pattern + "': " + err.Error())
|
||||
}
|
||||
return regex
|
||||
}
|
||||
|
||||
// Precompile compiles and caches multiple patterns at once
|
||||
func (c *RegexCache) Precompile(patterns []string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for _, pattern := range patterns {
|
||||
if _, exists := c.patterns[pattern]; !exists {
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.patterns[pattern] = regex
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the number of cached patterns
|
||||
func (c *RegexCache) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.patterns)
|
||||
}
|
||||
|
||||
// Clear removes all cached patterns
|
||||
func (c *RegexCache) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.patterns = make(map[string]*regexp.Regexp)
|
||||
}
|
||||
|
||||
// Global regex cache instance
|
||||
var globalCache = NewRegexCache()
|
||||
|
||||
// Common regex patterns used throughout the OIDC implementation
|
||||
const (
|
||||
// Email validation pattern (RFC 5322 compliant)
|
||||
EmailPattern = `^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
|
||||
|
||||
// Domain validation pattern
|
||||
DomainPattern = `^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
|
||||
|
||||
// URL validation pattern (http/https)
|
||||
URLPattern = `^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`
|
||||
|
||||
// JWT token pattern (three base64url parts separated by dots)
|
||||
JWTPattern = `^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$`
|
||||
|
||||
// Bearer token pattern (Authorization header)
|
||||
BearerTokenPattern = `^Bearer\s+([A-Za-z0-9._~+/-]+=*)$`
|
||||
|
||||
// Client ID pattern (alphanumeric with common separators)
|
||||
ClientIDPattern = `^[a-zA-Z0-9._-]+$`
|
||||
|
||||
// Scope pattern (space-separated alphanumeric with underscores)
|
||||
ScopePattern = `^[a-zA-Z0-9_]+(\s+[a-zA-Z0-9_]+)*$`
|
||||
|
||||
// Session ID pattern (hexadecimal)
|
||||
SessionIDPattern = `^[a-fA-F0-9]{32,128}$`
|
||||
|
||||
// CSRF token pattern (base64url)
|
||||
CSRFTokenPattern = `^[A-Za-z0-9_-]+$`
|
||||
|
||||
// Nonce pattern (base64url)
|
||||
NoncePattern = `^[A-Za-z0-9_-]+$`
|
||||
|
||||
// Code verifier pattern for PKCE (base64url, 43-128 chars)
|
||||
CodeVerifierPattern = `^[A-Za-z0-9_-]{43,128}$`
|
||||
|
||||
// Authorization code pattern (base64url)
|
||||
AuthCodePattern = `^[A-Za-z0-9._~+/-]+=*$`
|
||||
|
||||
// Redirect URI validation (must be absolute HTTP/HTTPS URL)
|
||||
RedirectURIPattern = `^https?://[^\s/$.?#].[^\s]*$`
|
||||
|
||||
// User-Agent pattern for bot detection
|
||||
BotUserAgentPattern = `(?i)(bot|crawler|spider|scraper|curl|wget|python|java|go-http)`
|
||||
|
||||
// IP address pattern (IPv4)
|
||||
IPv4Pattern = `^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`
|
||||
|
||||
// Tenant ID pattern (UUID format for Azure, etc.)
|
||||
TenantIDPattern = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`
|
||||
)
|
||||
|
||||
// Precompiled common patterns for immediate use
|
||||
var (
|
||||
EmailRegex *regexp.Regexp
|
||||
DomainRegex *regexp.Regexp
|
||||
URLRegex *regexp.Regexp
|
||||
JWTRegex *regexp.Regexp
|
||||
BearerTokenRegex *regexp.Regexp
|
||||
ClientIDRegex *regexp.Regexp
|
||||
ScopeRegex *regexp.Regexp
|
||||
SessionIDRegex *regexp.Regexp
|
||||
CSRFTokenRegex *regexp.Regexp
|
||||
NonceRegex *regexp.Regexp
|
||||
CodeVerifierRegex *regexp.Regexp
|
||||
AuthCodeRegex *regexp.Regexp
|
||||
RedirectURIRegex *regexp.Regexp
|
||||
BotUserAgentRegex *regexp.Regexp
|
||||
IPv4Regex *regexp.Regexp
|
||||
TenantIDRegex *regexp.Regexp
|
||||
)
|
||||
|
||||
// Initialize precompiled patterns
|
||||
func init() {
|
||||
commonPatterns := []string{
|
||||
EmailPattern,
|
||||
DomainPattern,
|
||||
URLPattern,
|
||||
JWTPattern,
|
||||
BearerTokenPattern,
|
||||
ClientIDPattern,
|
||||
ScopePattern,
|
||||
SessionIDPattern,
|
||||
CSRFTokenPattern,
|
||||
NoncePattern,
|
||||
CodeVerifierPattern,
|
||||
AuthCodePattern,
|
||||
RedirectURIPattern,
|
||||
BotUserAgentPattern,
|
||||
IPv4Pattern,
|
||||
TenantIDPattern,
|
||||
}
|
||||
|
||||
if err := globalCache.Precompile(commonPatterns); err != nil {
|
||||
panic("Failed to precompile common regex patterns: " + err.Error())
|
||||
}
|
||||
|
||||
// Assign precompiled patterns to global variables for easy access
|
||||
EmailRegex = globalCache.MustGet(EmailPattern)
|
||||
DomainRegex = globalCache.MustGet(DomainPattern)
|
||||
URLRegex = globalCache.MustGet(URLPattern)
|
||||
JWTRegex = globalCache.MustGet(JWTPattern)
|
||||
BearerTokenRegex = globalCache.MustGet(BearerTokenPattern)
|
||||
ClientIDRegex = globalCache.MustGet(ClientIDPattern)
|
||||
ScopeRegex = globalCache.MustGet(ScopePattern)
|
||||
SessionIDRegex = globalCache.MustGet(SessionIDPattern)
|
||||
CSRFTokenRegex = globalCache.MustGet(CSRFTokenPattern)
|
||||
NonceRegex = globalCache.MustGet(NoncePattern)
|
||||
CodeVerifierRegex = globalCache.MustGet(CodeVerifierPattern)
|
||||
AuthCodeRegex = globalCache.MustGet(AuthCodePattern)
|
||||
RedirectURIRegex = globalCache.MustGet(RedirectURIPattern)
|
||||
BotUserAgentRegex = globalCache.MustGet(BotUserAgentPattern)
|
||||
IPv4Regex = globalCache.MustGet(IPv4Pattern)
|
||||
TenantIDRegex = globalCache.MustGet(TenantIDPattern)
|
||||
}
|
||||
|
||||
// Global helper functions for common validations
|
||||
|
||||
// ValidateEmail checks if an email address is valid
|
||||
func ValidateEmail(email string) bool {
|
||||
return EmailRegex.MatchString(email)
|
||||
}
|
||||
|
||||
// ValidateDomain checks if a domain name is valid
|
||||
func ValidateDomain(domain string) bool {
|
||||
return DomainRegex.MatchString(domain)
|
||||
}
|
||||
|
||||
// ValidateURL checks if a URL is valid (http/https)
|
||||
func ValidateURL(url string) bool {
|
||||
return URLRegex.MatchString(url)
|
||||
}
|
||||
|
||||
// ValidateJWT checks if a token has valid JWT format
|
||||
func ValidateJWT(token string) bool {
|
||||
return JWTRegex.MatchString(token)
|
||||
}
|
||||
|
||||
// ExtractBearerToken extracts the token from a Bearer authorization header
|
||||
func ExtractBearerToken(authHeader string) (string, bool) {
|
||||
matches := BearerTokenRegex.FindStringSubmatch(authHeader)
|
||||
if len(matches) == 2 {
|
||||
return matches[1], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ValidateClientID checks if a client ID has valid format
|
||||
func ValidateClientID(clientID string) bool {
|
||||
return ClientIDRegex.MatchString(clientID)
|
||||
}
|
||||
|
||||
// ValidateScopes checks if scopes string has valid format
|
||||
func ValidateScopes(scopes string) bool {
|
||||
return ScopeRegex.MatchString(scopes)
|
||||
}
|
||||
|
||||
// ValidateSessionID checks if a session ID has valid format
|
||||
func ValidateSessionID(sessionID string) bool {
|
||||
return SessionIDRegex.MatchString(sessionID)
|
||||
}
|
||||
|
||||
// ValidateCSRFToken checks if a CSRF token has valid format
|
||||
func ValidateCSRFToken(token string) bool {
|
||||
return CSRFTokenRegex.MatchString(token)
|
||||
}
|
||||
|
||||
// ValidateNonce checks if a nonce has valid format
|
||||
func ValidateNonce(nonce string) bool {
|
||||
return NonceRegex.MatchString(nonce)
|
||||
}
|
||||
|
||||
// ValidateCodeVerifier checks if a PKCE code verifier has valid format
|
||||
func ValidateCodeVerifier(verifier string) bool {
|
||||
return CodeVerifierRegex.MatchString(verifier)
|
||||
}
|
||||
|
||||
// ValidateAuthCode checks if an authorization code has valid format
|
||||
func ValidateAuthCode(code string) bool {
|
||||
return AuthCodeRegex.MatchString(code)
|
||||
}
|
||||
|
||||
// ValidateRedirectURI checks if a redirect URI is valid
|
||||
func ValidateRedirectURI(uri string) bool {
|
||||
return RedirectURIRegex.MatchString(uri)
|
||||
}
|
||||
|
||||
// IsBotUserAgent checks if a User-Agent suggests an automated client
|
||||
func IsBotUserAgent(userAgent string) bool {
|
||||
return BotUserAgentRegex.MatchString(userAgent)
|
||||
}
|
||||
|
||||
// ValidateIPv4 checks if an IP address is valid IPv4
|
||||
func ValidateIPv4(ip string) bool {
|
||||
return IPv4Regex.MatchString(ip)
|
||||
}
|
||||
|
||||
// ValidateTenantID checks if a tenant ID has valid UUID format
|
||||
func ValidateTenantID(tenantID string) bool {
|
||||
return TenantIDRegex.MatchString(tenantID)
|
||||
}
|
||||
|
||||
// GetGlobalCache returns the global regex cache instance
|
||||
func GetGlobalCache() *RegexCache {
|
||||
return globalCache
|
||||
}
|
||||
|
||||
// CompilePattern compiles a pattern using the global cache
|
||||
func CompilePattern(pattern string) (*regexp.Regexp, error) {
|
||||
return globalCache.Get(pattern)
|
||||
}
|
||||
|
||||
// MustCompilePattern compiles a pattern using the global cache, panicking on error
|
||||
func MustCompilePattern(pattern string) *regexp.Regexp {
|
||||
return globalCache.MustGet(pattern)
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package patterns
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegexCache_Get(t *testing.T) {
|
||||
cache := NewRegexCache()
|
||||
|
||||
pattern := `^test\d+$`
|
||||
|
||||
// First call should compile and cache
|
||||
regex1, err := cache.Get(pattern)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get regex: %v", err)
|
||||
}
|
||||
|
||||
// Second call should return cached version
|
||||
regex2, err := cache.Get(pattern)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cached regex: %v", err)
|
||||
}
|
||||
|
||||
// Should be the same instance
|
||||
if regex1 != regex2 {
|
||||
t.Error("Expected same regex instance from cache")
|
||||
}
|
||||
|
||||
// Test the regex works
|
||||
if !regex1.MatchString("test123") {
|
||||
t.Error("Regex should match 'test123'")
|
||||
}
|
||||
|
||||
if regex1.MatchString("test") {
|
||||
t.Error("Regex should not match 'test'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCache_ConcurrentAccess(t *testing.T) {
|
||||
cache := NewRegexCache()
|
||||
pattern := `^concurrent\d+$`
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]*regexp.Regexp, 10)
|
||||
errors := make([]error, 10)
|
||||
|
||||
// Launch multiple goroutines to access the same pattern
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
regex, err := cache.Get(pattern)
|
||||
results[index] = regex
|
||||
errors[index] = err
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check all succeeded
|
||||
for i, err := range errors {
|
||||
if err != nil {
|
||||
t.Fatalf("Goroutine %d failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// All should return the same instance
|
||||
first := results[0]
|
||||
for i, regex := range results[1:] {
|
||||
if regex != first {
|
||||
t.Errorf("Goroutine %d got different regex instance", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCache_InvalidPattern(t *testing.T) {
|
||||
cache := NewRegexCache()
|
||||
|
||||
_, err := cache.Get(`[invalid`)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid regex pattern")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexCache_Precompile(t *testing.T) {
|
||||
cache := NewRegexCache()
|
||||
|
||||
patterns := []string{
|
||||
`^test1$`,
|
||||
`^test2$`,
|
||||
`^test3$`,
|
||||
}
|
||||
|
||||
err := cache.Precompile(patterns)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to precompile patterns: %v", err)
|
||||
}
|
||||
|
||||
if cache.Size() != 3 {
|
||||
t.Errorf("Expected cache size 3, got %d", cache.Size())
|
||||
}
|
||||
|
||||
// Should be able to get precompiled patterns without error
|
||||
for _, pattern := range patterns {
|
||||
_, err := cache.Get(pattern)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get precompiled pattern %s: %v", pattern, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationFunctions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
function func(string) bool
|
||||
valid []string
|
||||
invalid []string
|
||||
}{
|
||||
{
|
||||
name: "ValidateEmail",
|
||||
function: ValidateEmail,
|
||||
valid: []string{"[email protected]", "[email protected]", "[email protected]"},
|
||||
invalid: []string{"invalid-email", "@domain.com", "user@", ""},
|
||||
},
|
||||
{
|
||||
name: "ValidateDomain",
|
||||
function: ValidateDomain,
|
||||
valid: []string{"example.com", "sub.domain.org", "test.co.uk"},
|
||||
invalid: []string{"", "invalid..domain", ".example.com", "domain."},
|
||||
},
|
||||
{
|
||||
name: "ValidateJWT",
|
||||
function: ValidateJWT,
|
||||
valid: []string{"eyJ0.eyJ1.sig", "a.b.c"},
|
||||
invalid: []string{"invalid", "a.b", "a.b.c.d", ""},
|
||||
},
|
||||
{
|
||||
name: "ValidateClientID",
|
||||
function: ValidateClientID,
|
||||
valid: []string{"client123", "my-client_id", "123.456"},
|
||||
invalid: []string{"", "client with spaces", "client@invalid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, valid := range tt.valid {
|
||||
if !tt.function(valid) {
|
||||
t.Errorf("%s should be valid: %s", tt.name, valid)
|
||||
}
|
||||
}
|
||||
|
||||
for _, invalid := range tt.invalid {
|
||||
if tt.function(invalid) {
|
||||
t.Errorf("%s should be invalid: %s", tt.name, invalid)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBearerToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
header string
|
||||
expected string
|
||||
valid bool
|
||||
}{
|
||||
{"Bearer abc123", "abc123", true},
|
||||
{"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9", true},
|
||||
{"bearer token123", "", false}, // case sensitive
|
||||
{"Basic abc123", "", false},
|
||||
{"Bearer", "", false},
|
||||
{"", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
token, valid := ExtractBearerToken(tt.header)
|
||||
if valid != tt.valid {
|
||||
t.Errorf("ExtractBearerToken(%q) valid = %v, want %v", tt.header, valid, tt.valid)
|
||||
}
|
||||
if token != tt.expected {
|
||||
t.Errorf("ExtractBearerToken(%q) token = %q, want %q", tt.header, token, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRegexCache_Get(b *testing.B) {
|
||||
cache := NewRegexCache()
|
||||
pattern := `^benchmark\d+$`
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, err := cache.Get(pattern)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkRegexCache_Validation(b *testing.B) {
|
||||
email := "[email protected]"
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
ValidateEmail(email)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkRegex_DirectCompile(b *testing.B) {
|
||||
pattern := `^benchmark\d+$`
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user