134 lines
3.7 KiB
Go
134 lines
3.7 KiB
Go
package automation
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
)
|
||
|
||
// applyFilters implements the "filters" ssm feature: for each rule, it
|
||
// scopes down to the proxies matched by rule.Match, decides which of those
|
||
// are unwanted, and strips them out of every proxy-group's `proxies` list
|
||
// in finalConfig — both groups merged in from subscriptions and ones built
|
||
// by a proxy-groups rule. A scoped proxy is unwanted if rule.Compares is
|
||
// empty (Match alone acts as a denylist), or if it satisfies every entry in
|
||
// Compares (AND'd) when Compares is non-empty; a proxy whose name doesn't
|
||
// match a Compares entry's Pattern at all can't be evaluated and is left
|
||
// alone. Returns one error per rule with an invalid pattern/operator/match;
|
||
// other rules still run.
|
||
func applyFilters(finalConfig map[string]interface{}, rules []FilterRule, proxies []ProxyRef) []error {
|
||
var errs []error
|
||
|
||
for _, rule := range rules {
|
||
candidates, err := matchProxies(proxies, rule.Match)
|
||
if err != nil {
|
||
errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err))
|
||
continue
|
||
}
|
||
|
||
compares, err := compileCompares(rule.Compares)
|
||
if err != nil {
|
||
errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err))
|
||
continue
|
||
}
|
||
|
||
unwanted := make(map[string]bool, len(candidates))
|
||
for _, p := range candidates {
|
||
if len(compares) == 0 || satisfiesCompares(compares, p.OriginalName) {
|
||
unwanted[p.DisplayName] = true
|
||
}
|
||
}
|
||
if len(unwanted) == 0 {
|
||
fmt.Printf("ℹ️ ssm: filter '%s' matched 0 proxies to remove\n", rule.Name)
|
||
continue
|
||
}
|
||
|
||
removeFromProxyGroups(finalConfig, unwanted)
|
||
}
|
||
|
||
return errs
|
||
}
|
||
|
||
// removeFromProxyGroups strips every proxy in unwanted out of each proxy
|
||
// group's `proxies` list, leaving group references, DIRECT/REJECT, etc.
|
||
// untouched.
|
||
func removeFromProxyGroups(finalConfig map[string]interface{}, unwanted map[string]bool) {
|
||
groups, _ := finalConfig["proxy-groups"].([]interface{})
|
||
for _, g := range groups {
|
||
group, ok := g.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
list, ok := group["proxies"].([]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
kept := make([]interface{}, 0, len(list))
|
||
for _, item := range list {
|
||
if name, ok := item.(string); ok && unwanted[name] {
|
||
continue
|
||
}
|
||
kept = append(kept, item)
|
||
}
|
||
group["proxies"] = kept
|
||
}
|
||
}
|
||
|
||
type compiledCompare struct {
|
||
re *regexp.Regexp
|
||
cmp func(a, b float64) bool
|
||
value float64
|
||
}
|
||
|
||
func compileCompares(rules []CompareRule) ([]compiledCompare, error) {
|
||
compiled := make([]compiledCompare, 0, len(rules))
|
||
for _, c := range rules {
|
||
re, err := regexp.Compile(c.Pattern)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("invalid pattern %q: %w", c.Pattern, err)
|
||
}
|
||
cmp, err := comparator(c.Operator)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
compiled = append(compiled, compiledCompare{re: re, cmp: cmp, value: c.Value})
|
||
}
|
||
return compiled, nil
|
||
}
|
||
|
||
func satisfiesCompares(compares []compiledCompare, name string) bool {
|
||
for _, c := range compares {
|
||
m := c.re.FindStringSubmatch(name)
|
||
if len(m) < 2 {
|
||
return false
|
||
}
|
||
extracted, err := strconv.ParseFloat(m[1], 64)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
if !c.cmp(extracted, c.value) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func comparator(op string) (func(a, b float64) bool, error) {
|
||
switch op {
|
||
case "<":
|
||
return func(a, b float64) bool { return a < b }, nil
|
||
case "<=":
|
||
return func(a, b float64) bool { return a <= b }, nil
|
||
case ">":
|
||
return func(a, b float64) bool { return a > b }, nil
|
||
case ">=":
|
||
return func(a, b float64) bool { return a >= b }, nil
|
||
case "==", "=":
|
||
return func(a, b float64) bool { return a == b }, nil
|
||
case "!=":
|
||
return func(a, b float64) bool { return a != b }, nil
|
||
default:
|
||
return nil, fmt.Errorf("unknown operator %q (expected <, <=, >, >=, ==, !=)", op)
|
||
}
|
||
}
|