mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +00:00
c3f23cb99b
* Resolve issue with opaque tokens not being parsed correctly * Increase test coverage * Further improvements to test coverage and code quality * Add new providers. * fixup! Add new providers. * Cleanup. * fixup! Cleanup. * fixup! fixup! Cleanup. * fixup! fixup! fixup! Cleanup. * fixup! fixup! fixup! fixup! Cleanup. * Memory management optimisation 24 bytes per Put < 256-4096 bytes per buffer allocation avoided (10-170x difference) * Pooling cleanup.
310 lines
9.0 KiB
Go
310 lines
9.0 KiB
Go
// 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)
|
|
}
|