Files
bifrost/transports/bifrost-http/lib/headermatcher.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

137 lines
3.8 KiB
Go

package lib
import (
"strings"
configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
)
// HeaderMatchesPattern returns true if headerName matches the pattern.
// Patterns support trailing wildcard: "anthropic-*" matches "anthropic-beta".
// A bare "*" matches everything. All comparisons are case-insensitive.
func HeaderMatchesPattern(pattern, headerName string) bool {
pattern = strings.ToLower(strings.TrimSpace(pattern))
headerName = strings.ToLower(strings.TrimSpace(headerName))
if pattern == "*" {
return true
}
if strings.HasSuffix(pattern, "*") {
return strings.HasPrefix(headerName, pattern[:len(pattern)-1])
}
return pattern == headerName
}
// HeaderMatcher holds precomputed header filter data for O(1) exact-match lookups
// and fast prefix matching. Compiled once on config change, safe for concurrent reads.
type HeaderMatcher struct {
allowExact map[string]bool
allowPrefixes []string // lowercased prefixes (without trailing *)
allowAll bool
hasAllowlist bool
denyExact map[string]bool
denyPrefixes []string
denyAll bool
hasDenylist bool
}
// NewHeaderMatcher compiles a GlobalHeaderFilterConfig into an optimized HeaderMatcher.
// Returns nil if config is nil (callers should treat nil as "allow all").
func NewHeaderMatcher(config *configstoreTables.GlobalHeaderFilterConfig) *HeaderMatcher {
if config == nil {
return nil
}
m := &HeaderMatcher{
allowExact: make(map[string]bool, len(config.Allowlist)),
denyExact: make(map[string]bool, len(config.Denylist)),
}
for _, p := range config.Allowlist {
lp := strings.ToLower(strings.TrimSpace(p))
if lp == "" {
continue
}
if lp == "*" {
m.allowAll = true
} else if strings.HasSuffix(lp, "*") {
m.allowPrefixes = append(m.allowPrefixes, lp[:len(lp)-1])
} else {
m.allowExact[lp] = true
}
}
for _, p := range config.Denylist {
lp := strings.ToLower(strings.TrimSpace(p))
if lp == "" {
continue
}
if lp == "*" {
m.denyAll = true
} else if strings.HasSuffix(lp, "*") {
m.denyPrefixes = append(m.denyPrefixes, lp[:len(lp)-1])
} else {
m.denyExact[lp] = true
}
}
m.hasAllowlist = m.allowAll || len(m.allowExact) > 0 || len(m.allowPrefixes) > 0
m.hasDenylist = m.denyAll || len(m.denyExact) > 0 || len(m.denyPrefixes) > 0
return m
}
// HasAllowlist returns true if the matcher has a non-empty allowlist.
func (m *HeaderMatcher) HasAllowlist() bool {
if m == nil {
return false
}
return m.hasAllowlist
}
// MatchesAllow returns true if headerName matches any allowlist entry.
// headerName must be lowercased by the caller.
func (m *HeaderMatcher) MatchesAllow(headerName string) bool {
if m.allowAll {
return true
}
if m.allowExact[headerName] {
return true
}
for _, prefix := range m.allowPrefixes {
if strings.HasPrefix(headerName, prefix) {
return true
}
}
return false
}
// MatchesDeny returns true if headerName matches any denylist entry.
// headerName must be lowercased by the caller.
func (m *HeaderMatcher) MatchesDeny(headerName string) bool {
if m.denyAll {
return true
}
if m.denyExact[headerName] {
return true
}
for _, prefix := range m.denyPrefixes {
if strings.HasPrefix(headerName, prefix) {
return true
}
}
return false
}
// ShouldAllow determines if a header should be forwarded based on the
// configurable header filter config (separate from the security denylist).
// Returns true if the header passes both allowlist and denylist checks.
// headerName is lowercased internally for case-insensitive matching.
func (m *HeaderMatcher) ShouldAllow(headerName string) bool {
if m == nil {
return true
}
headerName = strings.ToLower(headerName)
if m.hasAllowlist && !m.MatchesAllow(headerName) {
return false
}
if m.hasDenylist && m.MatchesDeny(headerName) {
return false
}
return true
}