Files
ss/internal/automation/filter.go

134 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}