proxy-pool/internal/domain/routing/rule.go
youfak 1846c98e09
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: consume gateway routing snapshots
2026-07-31 15:36:56 +08:00

165 lines
3.8 KiB
Go

package routing
import (
"fmt"
"regexp"
"slices"
"strings"
)
type Action string
const (
ActionProxy Action = "proxy"
ActionDirect Action = "direct"
ActionReject Action = "reject"
)
type StrategyType string
const (
StrategySequential StrategyType = "sequential"
StrategyRandom StrategyType = "random"
StrategyRoundRobin StrategyType = "roundRobin"
StrategyWeighted StrategyType = "weighted"
StrategyLeastConnections StrategyType = "leastConnections"
)
type Strategy struct {
Type StrategyType
CurrentUpstream string
Weights map[string]uint32
}
type OnUnavailableAction string
const (
OnUnavailableReject OnUnavailableAction = "reject"
OnUnavailableWait OnUnavailableAction = "wait"
OnUnavailableDirect OnUnavailableAction = "direct"
)
type Match struct {
HostRegex string
Methods []string
PathRegex string
Headers map[string]string
}
type Rule struct {
Name string
Match Match
Upstreams []string
Action Action
Strategy Strategy
OnUnavailable OnUnavailableAction
}
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)
}
}
owned := cloneRule(rule)
if owned.Action == "" && len(owned.Upstreams) > 0 {
owned.Action = ActionProxy
}
compiled = append(compiled, compiledRule{rule: owned, 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 cloneRule(candidate.rule), true
}
return Rule{}, false
}
func cloneRule(source Rule) Rule {
cloned := source
cloned.Match.Methods = append([]string(nil), source.Match.Methods...)
cloned.Upstreams = append([]string(nil), source.Upstreams...)
if source.Match.Headers != nil {
cloned.Match.Headers = make(map[string]string, len(source.Match.Headers))
for name, value := range source.Match.Headers {
cloned.Match.Headers[name] = value
}
}
if source.Strategy.Weights != nil {
cloned.Strategy.Weights = make(map[string]uint32, len(source.Strategy.Weights))
for upstream, weight := range source.Strategy.Weights {
cloned.Strategy.Weights[upstream] = weight
}
}
return cloned
}
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
}