// Package utils provides common utility functions used across the OIDC middleware package utils import ( "fmt" "net/http" "os" "runtime" "strings" ) // CreateStringMap creates a map with string keys for efficient lookups func CreateStringMap(items []string) map[string]struct{} { result := make(map[string]struct{}) for _, item := range items { result[item] = struct{}{} } return result } // CreateCaseInsensitiveStringMap creates a map with lowercase keys for case-insensitive matching func CreateCaseInsensitiveStringMap(items []string) map[string]struct{} { result := make(map[string]struct{}) for _, item := range items { result[strings.ToLower(item)] = struct{}{} } return result } // DeduplicateScopes removes duplicate scopes from a slice func DeduplicateScopes(scopes []string) []string { seen := make(map[string]bool) result := []string{} for _, scope := range scopes { if !seen[scope] { seen[scope] = true result = append(result, scope) } } return result } // MergeScopes combines default scopes with user-provided scopes, removing duplicates func MergeScopes(defaultScopes, userScopes []string) []string { if len(userScopes) == 0 { return append([]string(nil), defaultScopes...) } seen := make(map[string]bool) var result []string for _, scope := range defaultScopes { if !seen[scope] { seen[scope] = true result = append(result, scope) } } for _, scope := range userScopes { if !seen[scope] { seen[scope] = true result = append(result, scope) } } return result } // IsTestMode detects if the code is running in a test environment func IsTestMode() bool { if os.Getenv("SUPPRESS_DIAGNOSTIC_LOGS") == "1" { return true } if strings.Contains(os.Args[0], ".test") || strings.Contains(os.Args[0], "go_build_") || os.Getenv("GO_TEST") == "1" || runtime.Compiler == "yaegi" { return true } for _, arg := range os.Args { if strings.Contains(arg, "-test") { return true } } if runtime.Compiler == "gc" { progName := os.Args[0] if strings.Contains(progName, "test") || strings.HasSuffix(progName, ".test") || strings.Contains(progName, "__debug_bin") { return true } } // Only use runtime stack check as fallback when no explicit test conditions are being controlled if os.Getenv("DISABLE_RUNTIME_STACK_CHECK") != "1" && os.Getenv("SUPPRESS_DIAGNOSTIC_LOGS") == "" && os.Getenv("GO_TEST") == "" { // Check runtime stack for test functions only as last resort buf := make([]byte, 2048) n := runtime.Stack(buf, false) stack := string(buf[:n]) if strings.Contains(stack, "testing.tRunner") || strings.Contains(stack, "testing.(*T)") || strings.Contains(stack, ".test.") { return true } } return false } // KeysFromMap extracts string keys from a map for logging purposes func KeysFromMap(m map[string]struct{}) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } // DetermineScheme determines the URL scheme for building redirect URLs. // Priority order (highest to lowest): // 1. forceHTTPS parameter - explicit security requirement // 2. X-Forwarded-Proto header - proxy/load balancer information // 3. TLS connection state - direct HTTPS connection // 4. Default to http // // The forceHTTPS parameter ensures redirect URIs use HTTPS even when behind // proxies/load balancers that may overwrite X-Forwarded-Proto header // (e.g., AWS ALB terminating TLS). func DetermineScheme(req *http.Request, forceHTTPS bool) string { // Honor forceHTTPS configuration as highest priority if forceHTTPS { return "https" } // Check X-Forwarded-Proto header for proxy scenarios if scheme := req.Header.Get("X-Forwarded-Proto"); scheme != "" { return scheme } // Check if connection has TLS if req.TLS != nil { return "https" } // Default to http return "http" } // DetermineHost determines the host for building redirect URLs. // It checks X-Forwarded-Host header first (for proxy scenarios), // then falls back to req.Host. func DetermineHost(req *http.Request) string { if host := sanitizeForwardedHost(req.Header.Get("X-Forwarded-Host")); host != "" { return host } return req.Host } // sanitizeForwardedHost returns a single, well-formed host from a (possibly // comma-separated) X-Forwarded-Host header, or "" if none is usable. It takes // only the first value and rejects whitespace and control characters, so a // crafted header cannot inject CRLF, smuggle a second host, or otherwise poison // the redirect URLs built from the result. func sanitizeForwardedHost(v string) string { if v == "" { return "" } if i := strings.IndexByte(v, ','); i >= 0 { v = v[:i] } v = strings.TrimSpace(v) if v == "" { return "" } if strings.IndexFunc(v, func(r rune) bool { return r < 0x20 || r == 0x7f || r == ' ' }) >= 0 { return "" } return v } // BuildFullURL constructs a URL from scheme, host, and path components. // It handles absolute URLs (returning them as-is) and ensures paths have leading slashes. func BuildFullURL(scheme, host, path string) string { if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { return path } if !strings.HasPrefix(path, "/") { path = "/" + path } return fmt.Sprintf("%s://%s%s", scheme, host, path) }