first commit
This commit is contained in:
136
transports/bifrost-http/lib/headermatcher.go
Normal file
136
transports/bifrost-http/lib/headermatcher.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user