Files
traefikoidc/internal/utils/utils.go
T
lukaszraczylo 4d23d618f5 fix(cache): harden cache backends, resilience, recovery, and transports
- RESP -ERR no longer closes healthy conn / aborts batch; global cap replacement-aware
- global health-check pass serialized (latent data race); MemoryMonitor via ResourceManager
- token-exchange retry, open-circuit rejections counted as failures
- TLS-posture matching transport reuse (no silent InsecureSkipVerify handoff)
2026-08-14 16:16:51 +01:00

213 lines
6.0 KiB
Go

// 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. Only trusted
// values are accepted: a malformed/junk scheme (e.g. an attacker-set
// "X-Forwarded-Proto: ftp") would otherwise be emitted verbatim into
// redirect_uri as a broken or weakened scheme. Invalid values fall
// through to the TLS check (R103).
if scheme := req.Header.Get("X-Forwarded-Proto"); scheme == "https" || scheme == "http" {
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 {
// Reject control chars, whitespace, and the delimiters that would
// let a crafted X-Forwarded-Host smuggle a path (''/''), query
// (''?''), fragment (''#''), or userinfo (''@'') into the
// host part of a redirect URL (which is passed to the IdP as
// redirect_uri / post_logout_redirect_uri and matched there).
// Valid host characters — alphanumerics, '-', '.', ':', '[', ']'
// (port / IPv6) — remain allowed.
return r < 0x20 || r == 0x7f || r == ' ' ||
r == '/' || r == '?' || r == '#' || r == '@' || 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)
}