proxy-pool/internal/domain/routing/rule.go

119 lines
2.5 KiB
Go

package routing
import (
"fmt"
"regexp"
"slices"
"strings"
)
type Action string
const (
ActionProxy Action = "proxy"
ActionDirect Action = "direct"
ActionReject Action = "reject"
)
type Match struct {
HostRegex string
Methods []string
PathRegex string
Headers map[string]string
}
type Rule struct {
Name string
Match Match
Upstreams []string
Action Action
}
type Request struct {
Host string
Method string
Path string
Headers map[string]string
}
type compiledRule struct {
rule Rule
host *regexp.Regexp
path *regexp.Regexp
}
type RuleSet struct {
rules []compiledRule
}
func Compile(rules []Rule) (*RuleSet, error) {
compiled := make([]compiledRule, 0, len(rules))
for _, rule := range rules {
if rule.Name == "" {
return nil, fmt.Errorf("compile routing: rule name is required")
}
host, err := regexp.Compile(rule.Match.HostRegex)
if err != nil {
return nil, fmt.Errorf("compile routing %q host: %w", rule.Name, err)
}
var path *regexp.Regexp
if rule.Match.PathRegex != "" {
path, err = regexp.Compile(rule.Match.PathRegex)
if err != nil {
return nil, fmt.Errorf("compile routing %q path: %w", rule.Name, err)
}
}
if rule.Action == "" && len(rule.Upstreams) > 0 {
rule.Action = ActionProxy
}
compiled = append(compiled, compiledRule{rule: rule, host: host, path: path})
}
return &RuleSet{rules: compiled}, nil
}
func (r *RuleSet) Match(request Request) (Rule, bool) {
if r == nil {
return Rule{}, false
}
host := strings.ToLower(strings.TrimSuffix(request.Host, "."))
method := strings.ToUpper(request.Method)
for _, candidate := range r.rules {
if !candidate.host.MatchString(host) {
continue
}
if len(candidate.rule.Match.Methods) > 0 && !containsFold(candidate.rule.Match.Methods, method) {
continue
}
if candidate.path != nil && !candidate.path.MatchString(request.Path) {
continue
}
if !headersMatch(candidate.rule.Match.Headers, request.Headers) {
continue
}
return candidate.rule, true
}
return Rule{}, false
}
func containsFold(values []string, target string) bool {
return slices.ContainsFunc(values, func(value string) bool {
return strings.EqualFold(value, target)
})
}
func headersMatch(expected, actual map[string]string) bool {
for name, value := range expected {
matched := false
for actualName, actualValue := range actual {
if strings.EqualFold(name, actualName) && actualValue == value {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}