mirror of
https://github.com/go-gost/x.git
synced 2026-10-20 00:17:32 +00:00
Adds a 3-parameter form to Header(), Query(), and BodyJSON() matchers
for numeric comparisons: Header("Content-Length", "gt", "1024"). Extracts
the duplicated comparison setup and dispatch into shared compareOp and
parseCompareParams helpers. Includes tests for all six operators across
header, query, and body JSON matchers.
655 lines
14 KiB
Go
655 lines
14 KiB
Go
package routing
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/go-gost/core/routing"
|
|
"github.com/go-gost/x/registry"
|
|
"github.com/go-gost/x/routing/rules"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
var (
|
|
defaultParser rules.Parser
|
|
)
|
|
|
|
func init() {
|
|
var matchers []string
|
|
for matcher := range httpFuncs {
|
|
matchers = append(matchers, matcher)
|
|
}
|
|
|
|
parser, err := rules.NewParser(matchers)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defaultParser = parser
|
|
}
|
|
|
|
type matcher struct {
|
|
// matchers tree structure reflecting the rule.
|
|
tree matchersTree
|
|
}
|
|
|
|
// NewMatcher parses a routing rule string and returns a routing.Matcher.
|
|
// The rule syntax supports boolean expressions with matchers like
|
|
// Host(`example.com`), Path(`/api`), Method(`GET`), etc., combined
|
|
// with &&, ||, and ! operators.
|
|
func NewMatcher(rule string) (routing.Matcher, error) {
|
|
parse, err := defaultParser.Parse(rule)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
|
}
|
|
|
|
buildTree, ok := parse.(rules.TreeBuilder)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
|
}
|
|
|
|
var matchers matchersTree
|
|
err = matchers.addRule(buildTree(), httpFuncs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error while adding rule %s: %w", rule, err)
|
|
}
|
|
|
|
return &matcher{
|
|
tree: matchers,
|
|
}, nil
|
|
}
|
|
|
|
func (m *matcher) Match(req *routing.Request) bool {
|
|
if m == nil {
|
|
return false
|
|
}
|
|
|
|
return m.tree.match(req)
|
|
}
|
|
|
|
// matchersTree represents the matchers tree structure.
|
|
type matchersTree struct {
|
|
// matcher is a matcher func used to match HTTP request properties.
|
|
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
|
|
// It is therefore mutually exclusive with left and right.
|
|
matcher func(*routing.Request) bool
|
|
// operator to combine the evaluation of left and right leaves.
|
|
operator string
|
|
// Mutually exclusive with matcher.
|
|
left *matchersTree
|
|
right *matchersTree
|
|
}
|
|
|
|
func (m *matchersTree) match(req *routing.Request) bool {
|
|
if m == nil {
|
|
// This should never happen as it should have been detected during parsing.
|
|
return false
|
|
}
|
|
|
|
if m.matcher != nil {
|
|
return m.matcher(req)
|
|
}
|
|
|
|
switch m.operator {
|
|
case "or":
|
|
return m.left.match(req) || m.right.match(req)
|
|
case "and":
|
|
return m.left.match(req) && m.right.match(req)
|
|
default:
|
|
// This should never happen as it should have been detected during parsing.
|
|
return false
|
|
}
|
|
}
|
|
|
|
type matcherFuncs map[string]func(*matchersTree, ...string) error
|
|
|
|
func (m *matchersTree) addRule(rule *rules.Tree, funcs matcherFuncs) error {
|
|
switch rule.Matcher {
|
|
case "and", "or":
|
|
m.operator = rule.Matcher
|
|
m.left = &matchersTree{}
|
|
err := m.left.addRule(rule.RuleLeft, funcs)
|
|
if err != nil {
|
|
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
|
|
}
|
|
|
|
m.right = &matchersTree{}
|
|
return m.right.addRule(rule.RuleRight, funcs)
|
|
default:
|
|
err := rules.CheckRule(rule)
|
|
if err != nil {
|
|
return fmt.Errorf("error while checking rule %s: %w", rule.Matcher, err)
|
|
}
|
|
|
|
err = funcs[rule.Matcher](m, rule.Value...)
|
|
if err != nil {
|
|
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
|
|
}
|
|
|
|
if rule.Not {
|
|
matcherFunc := m.matcher
|
|
m.matcher = func(req *routing.Request) bool {
|
|
return !matcherFunc(req)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var httpFuncs = map[string]func(*matchersTree, ...string) error{
|
|
"ClientIP": expectNParameters(clientIP, 1),
|
|
"Network": expectNParameters(network, 1),
|
|
"Proto": expectNParameters(proto, 1),
|
|
"Host": expectNParameters(host, 1),
|
|
"HostRegexp": expectNParameters(hostRegexp, 1),
|
|
"Method": expectNParameters(method, 1),
|
|
"Path": expectNParameters(path, 1),
|
|
"PathRegexp": expectNParameters(pathRegexp, 1),
|
|
"PathPrefix": expectNParameters(pathPrefix, 1),
|
|
"Header": expectNParameters(header, 1, 2, 3),
|
|
"HeaderRegexp": expectNParameters(headerRegexp, 1, 2),
|
|
"Query": expectNParameters(query, 1, 2, 3),
|
|
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
|
|
"BodyJSON": expectNParameters(bodyJSON, 2, 3),
|
|
"BodyRegexp": expectNParameters(bodyRegexp, 1),
|
|
"Bypass": expectNParameters(bypass, 1),
|
|
"Admission": expectNParameters(admission, 1),
|
|
}
|
|
|
|
func expectNParameters(fn func(*matchersTree, ...string) error, n ...int) func(*matchersTree, ...string) error {
|
|
return func(tree *matchersTree, s ...string) error {
|
|
if !slices.Contains(n, len(s)) {
|
|
return fmt.Errorf("unexpected number of parameters; got %d, expected one of %v", len(s), n)
|
|
}
|
|
|
|
return fn(tree, s...)
|
|
}
|
|
}
|
|
|
|
func clientIP(tree *matchersTree, clientIP ...string) error {
|
|
ip := net.ParseIP(clientIP[0])
|
|
|
|
var ipNet *net.IPNet
|
|
if ip == nil {
|
|
_, ipNet, _ = net.ParseCIDR(clientIP[0])
|
|
}
|
|
if ip == nil && ipNet == nil {
|
|
return fmt.Errorf("invalid value %q for ClientIP matcher", clientIP[0])
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.ClientIP == nil {
|
|
return false
|
|
}
|
|
|
|
if ip != nil {
|
|
return ip.Equal(req.ClientIP)
|
|
}
|
|
|
|
return ipNet.Contains(req.ClientIP)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func proto(tree *matchersTree, protos ...string) error {
|
|
proto := strings.ToLower(protos[0])
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
// logger.Default().Debugf("proto: %s %s", proto, req.Protocol)
|
|
return proto == req.Protocol
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func network(tree *matchersTree, networks ...string) error {
|
|
network := strings.ToLower(networks[0])
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
// Use prefix match so Network("tcp") matches "tcp", "tcp4", "tcp6"
|
|
// and Network("udp") matches "udp", "udp4", "udp6".
|
|
return strings.HasPrefix(req.Network, network)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func method(tree *matchersTree, methods ...string) error {
|
|
method := strings.ToUpper(methods[0])
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
return method == req.Method
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func host(tree *matchersTree, hosts ...string) error {
|
|
host := hosts[0]
|
|
|
|
if !IsASCII(host) {
|
|
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", host)
|
|
}
|
|
|
|
host = strings.ToLower(strings.TrimSpace(host))
|
|
|
|
if strings.HasPrefix(host, "*") {
|
|
host = host[1:]
|
|
if !strings.HasPrefix(host, ".") {
|
|
host = "." + host
|
|
}
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
// logger.Default().Debugf("host: %s %s", host, req.Host)
|
|
reqHost := strings.ToLower(strings.TrimSpace(parseHost(req.Host)))
|
|
if len(reqHost) == 0 {
|
|
return false
|
|
}
|
|
|
|
if reqHost == host {
|
|
return true
|
|
}
|
|
|
|
if host[0] == '.' && strings.HasSuffix(reqHost, host) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func hostRegexp(tree *matchersTree, hosts ...string) error {
|
|
host := hosts[0]
|
|
|
|
if !IsASCII(host) {
|
|
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
|
|
}
|
|
|
|
re, err := regexp.Compile(host)
|
|
if err != nil {
|
|
return fmt.Errorf("compiling HostRegexp matcher: %w", err)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
// logger.Default().Debugf("hostRegexp: %s %s", host, req.Host)
|
|
return re.MatchString(strings.ToLower(strings.TrimSpace(parseHost(req.Host))))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func path(tree *matchersTree, paths ...string) error {
|
|
path := paths[0]
|
|
|
|
if !strings.HasPrefix(path, "/") {
|
|
return fmt.Errorf("path %q does not start with a '/'", path)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
return req.Path == path
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func pathRegexp(tree *matchersTree, paths ...string) error {
|
|
path := paths[0]
|
|
|
|
re, err := regexp.Compile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("compiling PathRegexp matcher: %w", err)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
return re.MatchString(req.Path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func pathPrefix(tree *matchersTree, paths ...string) error {
|
|
path := paths[0]
|
|
|
|
if !strings.HasPrefix(path, "/") {
|
|
return fmt.Errorf("path %q does not start with a '/'", path)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
return strings.HasPrefix(req.Path, path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func header(tree *matchersTree, headers ...string) error {
|
|
key := http.CanonicalHeaderKey(headers[0])
|
|
|
|
op, compVal, err := parseCompareParams(headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if op != "" {
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.Header == nil {
|
|
return false
|
|
}
|
|
for _, v := range req.Header[key] {
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if compareOp(f, compVal, op) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var value string
|
|
var hasValue bool
|
|
if len(headers) >= 2 {
|
|
value = headers[1]
|
|
hasValue = true
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.Header == nil {
|
|
return false
|
|
}
|
|
|
|
values, ok := req.Header[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if !hasValue {
|
|
return true
|
|
}
|
|
|
|
for _, headerValue := range values {
|
|
if headerValue == value {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func headerRegexp(tree *matchersTree, headers ...string) error {
|
|
if len(headers) == 1 {
|
|
return header(tree, headers...)
|
|
}
|
|
|
|
key, value := http.CanonicalHeaderKey(headers[0]), headers[1]
|
|
|
|
re, err := regexp.Compile(value)
|
|
if err != nil {
|
|
return fmt.Errorf("compiling HeaderRegexp matcher: %w", err)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.Header == nil {
|
|
return false
|
|
}
|
|
|
|
for _, headerValue := range req.Header[key] {
|
|
if re.MatchString(headerValue) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func query(tree *matchersTree, queries ...string) error {
|
|
key := queries[0]
|
|
|
|
op, compVal, err := parseCompareParams(queries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if op != "" {
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.Query == nil {
|
|
return false
|
|
}
|
|
for _, v := range req.Query[key] {
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if compareOp(f, compVal, op) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var value string
|
|
var hasValue bool
|
|
if len(queries) >= 2 {
|
|
value = queries[1]
|
|
hasValue = true
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.Query == nil {
|
|
return false
|
|
}
|
|
|
|
values, ok := req.Query[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if !hasValue {
|
|
return true
|
|
}
|
|
|
|
return slices.Contains(values, value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func queryRegexp(tree *matchersTree, queries ...string) error {
|
|
if len(queries) == 1 {
|
|
return query(tree, queries...)
|
|
}
|
|
|
|
key, value := queries[0], queries[1]
|
|
|
|
re, err := regexp.Compile(value)
|
|
if err != nil {
|
|
return fmt.Errorf("compiling QueryRegexp matcher: %w", err)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.Query == nil {
|
|
return false
|
|
}
|
|
|
|
values, ok := req.Query[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
idx := slices.IndexFunc(values, func(value string) bool {
|
|
return re.MatchString(value)
|
|
})
|
|
|
|
return idx >= 0
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func bodyRegexp(tree *matchersTree, patterns ...string) error {
|
|
re, err := regexp.Compile(patterns[0])
|
|
if err != nil {
|
|
return fmt.Errorf("compiling BodyRegexp matcher: %w", err)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if len(req.Body) == 0 {
|
|
return false
|
|
}
|
|
return re.Match(req.Body)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func compareOp(f, compVal float64, op string) bool {
|
|
switch op {
|
|
case "gt":
|
|
return f > compVal
|
|
case "ge":
|
|
return f >= compVal
|
|
case "lt":
|
|
return f < compVal
|
|
case "le":
|
|
return f <= compVal
|
|
case "eq":
|
|
return f == compVal
|
|
case "ne":
|
|
return f != compVal
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseCompareParams(args []string) (op string, compVal float64, err error) {
|
|
if len(args) < 3 {
|
|
return
|
|
}
|
|
op = args[1]
|
|
switch op {
|
|
case "gt", "ge", "lt", "le", "eq", "ne":
|
|
default:
|
|
return "", 0, fmt.Errorf("unknown comparison operator %q", op)
|
|
}
|
|
v, err := strconv.ParseFloat(args[2], 64)
|
|
if err != nil {
|
|
return "", 0, fmt.Errorf("invalid compare value %q: %w", args[2], err)
|
|
}
|
|
return op, v, nil
|
|
}
|
|
|
|
func bodyJSON(tree *matchersTree, args ...string) error {
|
|
path := args[0]
|
|
|
|
op, compVal, err := parseCompareParams(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if op != "" {
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if len(req.Body) == 0 {
|
|
return false
|
|
}
|
|
f, err := strconv.ParseFloat(gjson.GetBytes(req.Body, path).String(), 64)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return compareOp(f, compVal, op)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
pattern := args[1]
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return fmt.Errorf("compiling BodyJSON pattern: %w", err)
|
|
}
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if len(req.Body) == 0 {
|
|
return false
|
|
}
|
|
return re.MatchString(gjson.GetBytes(req.Body, path).String())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func admission(tree *matchersTree, names ...string) error {
|
|
name := names[0]
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if req.ClientIP == nil {
|
|
return false
|
|
}
|
|
if adm := registry.AdmissionRegistry().Get(name); adm != nil {
|
|
return adm.Admit(context.Background(), "ip", req.ClientIP.String())
|
|
}
|
|
return false
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func bypass(tree *matchersTree, names ...string) error {
|
|
name := names[0]
|
|
|
|
tree.matcher = func(req *routing.Request) bool {
|
|
if bp := registry.BypassRegistry().Get(name); bp != nil {
|
|
v := bp.Contains(context.Background(), "tcp", req.Host)
|
|
return !v
|
|
}
|
|
return false
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsASCII checks if the given string contains only ASCII characters.
|
|
func IsASCII(s string) bool {
|
|
for i := range len(s) {
|
|
if s[i] >= utf8.RuneSelf {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func parseHost(addr string) string {
|
|
if !strings.Contains(addr, ":") {
|
|
// IPv4 without port or empty address
|
|
return addr
|
|
}
|
|
|
|
// IPv4 with port or IPv6
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
if addr[0] == '[' && addr[len(addr)-1] == ']' {
|
|
return addr[1 : len(addr)-1]
|
|
}
|
|
return addr
|
|
}
|
|
return host
|
|
}
|