add routing rule for node

This commit is contained in:
ginuerzh
2024-11-15 20:28:47 +08:00
parent 79b6b9138e
commit c2aedbabd9
11 changed files with 1071 additions and 145 deletions
+39 -30
View File
@@ -155,7 +155,7 @@ type SelectorConfig struct {
type AdmissionConfig struct {
Name string `json:"name"`
// DEPRECATED by whitelist since beta.4
// Deprecated: use whitelist instead
Reverse bool `yaml:",omitempty" json:"reverse,omitempty"`
Whitelist bool `yaml:",omitempty" json:"whitelist,omitempty"`
Matchers []string `yaml:",omitempty" json:"matchers,omitempty"`
@@ -168,7 +168,7 @@ type AdmissionConfig struct {
type BypassConfig struct {
Name string `json:"name"`
// DEPRECATED by whitelist since beta.4
// Deprecated: use whitelist instead
Reverse bool `yaml:",omitempty" json:"reverse,omitempty"`
Whitelist bool `yaml:",omitempty" json:"whitelist,omitempty"`
Matchers []string `yaml:",omitempty" json:"matchers,omitempty"`
@@ -347,7 +347,7 @@ type HandlerConfig struct {
}
type ForwarderConfig struct {
// DEPRECATED by hop field
// Deprecated: use hop instead
Name string `yaml:",omitempty" json:"name,omitempty"`
// the referenced hop name
Hop string `yaml:",omitempty" json:"hop,omitempty"`
@@ -361,18 +361,20 @@ type ForwardNodeConfig struct {
Network string `yaml:",omitempty" json:"network,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
// DEPRECATED by filter.protocol
// Deprecated: use matcher instead
Protocol string `yaml:",omitempty" json:"protocol,omitempty"`
// DEPRECATED by filter.host
// Deprecated: use matcher instead
Host string `yaml:",omitempty" json:"host,omitempty"`
// DEPRECATED by filter.path
// Deprecated: use matcher instead
Path string `yaml:",omitempty" json:"path,omitempty"`
// DEPRECATED by http.auth
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
Filter *NodeFilterConfig `yaml:",omitempty" json:"filter,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
// Deprecated: use matcher instead
Filter *NodeFilterConfig `yaml:",omitempty" json:"filter,omitempty"`
Matcher *NodeMatcherConfig `yaml:",omitempty" json:"matcher,omitempty"`
// Deprecated: use http.auth instead
Auth *AuthConfig `yaml:",omitempty" json:"auth,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
}
type HTTPURLRewriteConfig struct {
@@ -393,6 +395,11 @@ type NodeFilterConfig struct {
Path string `yaml:",omitempty" json:"path,omitempty"`
}
type NodeMatcherConfig struct {
Rule string `yaml:",omitempty" json:"rule,omitempty"`
Priority int `yaml:",omitempty" json:"priority,omitempty"`
}
type HTTPNodeConfig struct {
// rewrite host header
Host string `yaml:",omitempty" json:"host,omitempty"`
@@ -435,9 +442,9 @@ type SockOptsConfig struct {
type ServiceConfig struct {
Name string `json:"name"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
// DEPRECATED by metadata.interface since beta.5
// Deprecated: use metadata.interface instead
Interface string `yaml:",omitempty" json:"interface,omitempty"`
// DEPRECATED by metadata.so_mark since beta.5
// Deprecated: use metadata.so_mark instead
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
Admission string `yaml:",omitempty" json:"admission,omitempty"`
Admissions []string `yaml:",omitempty" json:"admissions,omitempty"`
@@ -510,22 +517,24 @@ type HopConfig struct {
}
type NodeConfig struct {
Name string `json:"name"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
Resolver string `yaml:",omitempty" json:"resolver,omitempty"`
Hosts string `yaml:",omitempty" json:"hosts,omitempty"`
Connector *ConnectorConfig `yaml:",omitempty" json:"connector,omitempty"`
Dialer *DialerConfig `yaml:",omitempty" json:"dialer,omitempty"`
Interface string `yaml:",omitempty" json:"interface,omitempty"`
Netns string `yaml:",omitempty" json:"netns,omitempty"`
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
Filter *NodeFilterConfig `yaml:",omitempty" json:"filter,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
Name string `json:"name"`
Addr string `yaml:",omitempty" json:"addr,omitempty"`
Network string `yaml:",omitempty" json:"network,omitempty"`
Bypass string `yaml:",omitempty" json:"bypass,omitempty"`
Bypasses []string `yaml:",omitempty" json:"bypasses,omitempty"`
Resolver string `yaml:",omitempty" json:"resolver,omitempty"`
Hosts string `yaml:",omitempty" json:"hosts,omitempty"`
Connector *ConnectorConfig `yaml:",omitempty" json:"connector,omitempty"`
Dialer *DialerConfig `yaml:",omitempty" json:"dialer,omitempty"`
Interface string `yaml:",omitempty" json:"interface,omitempty"`
Netns string `yaml:",omitempty" json:"netns,omitempty"`
SockOpts *SockOptsConfig `yaml:"sockopts,omitempty" json:"sockopts,omitempty"`
// Deprecated: use matcher instead
Filter *NodeFilterConfig `yaml:",omitempty" json:"filter,omitempty"`
Matcher *NodeMatcherConfig `yaml:",omitempty" json:"matcher,omitempty"`
HTTP *HTTPNodeConfig `yaml:",omitempty" json:"http,omitempty"`
TLS *TLSNodeConfig `yaml:",omitempty" json:"tls,omitempty"`
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
}
type Config struct {
+20
View File
@@ -22,6 +22,7 @@ import (
mdx "github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
"github.com/go-gost/x/registry"
"github.com/go-gost/x/routing"
)
func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.Node, error) {
@@ -175,6 +176,25 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No
opts = append(opts, chain.NodeFilterOption(settings))
}
if cfg.Matcher != nil {
priority := cfg.Matcher.Priority
if rule := strings.TrimSpace(cfg.Matcher.Rule); rule != "" {
if matcher, err := routing.NewMatcher(rule); err == nil {
log.Debugf("new matcher for node %s with rule %s", cfg.Name, cfg.Matcher.Rule)
if priority == 0 {
priority = len(cfg.Matcher.Rule)
}
opts = append(opts, chain.MatcherNodeOption(matcher))
} else {
log.Error(err)
priority = -1
}
}
opts = append(opts, chain.PriorityNodeOption(priority))
}
if cfg.HTTP != nil {
settings := &chain.HTTPNodeSettings{
Host: cfg.HTTP.Host,
+1
View File
@@ -375,6 +375,7 @@ func parseForwarder(cfg *config.ForwarderConfig, log logger.Logger) (hop.Hop, er
Bypass: node.Bypass,
Bypasses: node.Bypasses,
Filter: filter,
Matcher: node.Matcher,
HTTP: httpCfg,
TLS: node.TLS,
Metadata: node.Metadata,
+10 -3
View File
@@ -9,7 +9,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/go-gost/core v0.1.9
github.com/go-gost/core v0.1.10
github.com/go-gost/gosocks4 v0.0.1
github.com/go-gost/gosocks5 v0.4.2
github.com/go-gost/plugin v0.1.1
@@ -34,16 +34,20 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/vishvananda/netlink v1.1.0
github.com/vishvananda/netns v0.0.4
github.com/vulcand/predicate v1.2.0
github.com/xtaci/kcp-go/v5 v5.6.5
github.com/xtaci/smux v1.5.31
github.com/xtaci/tcpraw v1.2.25
github.com/yl2chen/cidranger v1.0.2
github.com/zalando/go-keyring v0.2.4
golang.org/x/crypto v0.27.0
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8
golang.org/x/net v0.29.0
golang.org/x/sys v0.25.0
golang.org/x/text v0.18.0
golang.org/x/time v0.5.0
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478
google.golang.org/grpc v1.67.1
@@ -63,6 +67,7 @@ require (
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/coreos/go-iptables v0.5.0 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
@@ -75,7 +80,9 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/reedsolomon v1.11.8 // indirect
@@ -91,6 +98,7 @@ require (
github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
@@ -111,10 +119,9 @@ require (
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
+14 -2
View File
@@ -34,6 +34,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
@@ -50,8 +51,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-gost/core v0.1.9 h1:qrKzhE9X2+zuNc8d4hRv4I8ZklwHAfja5HngrLucM2c=
github.com/go-gost/core v0.1.9/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
github.com/go-gost/core v0.1.10 h1:sONgU+BRbDE5owFh7IeJ1KBdG7JH6KmCAW0FpLoId2I=
github.com/go-gost/core v0.1.10/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s=
github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc=
github.com/go-gost/gosocks5 v0.4.2 h1:IianxHTkACPqCwiOAT3MHoMdSUl+SEPSRu1ikawC1Pc=
@@ -110,8 +111,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf h1:C1GPyPJrOlJlIrcaBBiBpDsqZena2Ks8spa5xZqr1XQ=
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf/go.mod h1:zXqxTI6jXDdKnlf8s+nT+3c8LrwUEy3yNpO4XJL90lA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -197,6 +202,7 @@ github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1H
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 h1:XU9hik0exChEmY92ALW4l9WnDodxLVS9yOSNh2SizaQ=
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601/go.mod h1:mttDPaeLm87u74HMrP+n2tugXvIKWcwff/cqSX0lehY=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
@@ -219,6 +225,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -243,6 +250,8 @@ github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYp
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50=
github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
github.com/xtaci/kcp-go/v5 v5.6.5 h1:oxGZNobj3OddrLzwdJYnR/waNgwrL98u02u0DWNHE3k=
github.com/xtaci/kcp-go/v5 v5.6.5/go.mod h1:Qy3Zf2tWTdFdEs0E8JvhrX+39r5UDZoYac8anvud7/Q=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
@@ -267,6 +276,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
@@ -290,6 +300,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
@@ -360,6 +371,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+77 -107
View File
@@ -14,6 +14,7 @@ import (
"github.com/go-gost/core/chain"
"github.com/go-gost/core/hop"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/routing"
"github.com/go-gost/core/selector"
"github.com/go-gost/x/config"
node_parser "github.com/go-gost/x/config/parsing/node"
@@ -132,18 +133,16 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
opt(&options)
}
log := p.options.logger
// hop level bypass
if p.options.bypass != nil &&
p.options.bypass.Contains(ctx, options.Network, options.Addr, bypass.WithHostOpton(options.Host)) {
return nil
}
filters := p.filterByHost(options.Host, p.Nodes()...)
filters = p.filterByProtocol(options.Protocol, filters...)
filters = p.filterByPath(options.Path, filters...)
var nodes []*chain.Node
for _, node := range filters {
for _, node := range p.Nodes() {
if node == nil {
continue
}
@@ -153,11 +152,42 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
continue
}
if matcher := node.Options().Matcher; matcher != nil {
req := routing.Request{
ClientIP: options.ClientIP,
Host: options.Host,
Protocol: options.Protocol,
Method: options.Method,
Path: options.Path,
Query: options.Query,
Header: options.Header,
}
if !matcher.Match(&req) {
continue
}
log.Debugf("node %s match request %s %s, priority %d", node.Name, req.Protocol, req.Host, node.Options().Priority)
} else {
if !p.isEligible(node, &options) {
continue
}
}
nodes = append(nodes, node)
}
if len(nodes) == 0 {
return nil
}
if len(nodes) == 1 {
return nodes[0]
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Options().Priority > nodes[j].Options().Priority
})
if nodes[0].Options().Priority > 0 {
return nodes[0]
}
if s := p.options.selector; s != nil {
return s.Select(ctx, nodes...)
@@ -165,126 +195,66 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
return nodes[0]
}
func (p *chainHop) filterByHost(host string, nodes ...*chain.Node) (filters []*chain.Node) {
if host == "" || len(nodes) == 0 {
return nodes
func (p *chainHop) isEligible(node *chain.Node, opts *hop.SelectOptions) bool {
if node == nil {
return false
}
if node.Options().Filter == nil {
return true
}
if !p.checkHost(opts.Host, node) || !p.checkProtocol(opts.Protocol, node) || !p.checkPath(opts.Path, node) {
return false
}
return true
}
func (p *chainHop) checkHost(host string, node *chain.Node) bool {
var vhost string
if filter := node.Options().Filter; filter != nil {
vhost = filter.Host
}
if vhost == "" { // backup node
return true
}
if host == "" {
return false
}
if v, _, _ := net.SplitHostPort(host); v != "" {
host = v
}
p.options.logger.Debugf("filter by host: %s", host)
found := false
for _, node := range nodes {
if node == nil {
continue
}
var vhost string
if filter := node.Options().Filter; filter != nil {
vhost = filter.Host
}
if vhost == "" { // backup node
if !found {
filters = append(filters, node)
}
continue
}
if vhost == host ||
vhost[0] == '.' && strings.HasSuffix(host, vhost[1:]) {
if !found { // clear all backup nodes when matched node found
filters = nil
}
filters = append(filters, node)
found = true
continue
}
if vhost == host || vhost[0] == '.' && strings.HasSuffix(host, vhost[1:]) {
return true
}
return
return false
}
func (p *chainHop) filterByProtocol(protocol string, nodes ...*chain.Node) (filters []*chain.Node) {
if protocol == "" || len(nodes) == 0 {
return nodes
func (p *chainHop) checkProtocol(protocol string, node *chain.Node) bool {
var prot string
if filter := node.Options().Filter; filter != nil {
prot = filter.Protocol
}
p.options.logger.Debugf("filter by protocol: %s", protocol)
found := false
for _, node := range nodes {
if node == nil {
continue
}
var prot string
if filter := node.Options().Filter; filter != nil {
prot = filter.Protocol
}
if prot == "" {
if !found {
filters = append(filters, node)
}
continue
}
if prot == protocol {
if !found {
filters = nil
}
filters = append(filters, node)
found = true
continue
}
if prot == "" {
return true
}
return
return prot == protocol
}
func (p *chainHop) filterByPath(path string, nodes ...*chain.Node) (filters []*chain.Node) {
if path == "" || len(nodes) == 0 {
return nodes
func (p *chainHop) checkPath(path string, node *chain.Node) bool {
var pathFilter string
if filter := node.Options().Filter; filter != nil {
pathFilter = filter.Path
}
p.options.logger.Debugf("filter by path: %s", path)
sort.SliceStable(nodes, func(i, j int) bool {
filter1 := nodes[i].Options().Filter
if filter1 == nil {
return false
}
filter2 := nodes[j].Options().Filter
if filter2 == nil {
return true
}
return len(filter1.Path) > len(filter2.Path)
})
found := false
for _, node := range nodes {
var pathFilter string
if filter := node.Options().Filter; filter != nil {
pathFilter = filter.Path
}
if pathFilter == "" {
if !found {
filters = append(filters, node)
}
continue
}
if strings.HasPrefix(path, pathFilter) {
if !found {
filters = nil
}
filters = append(filters, node)
break
}
if pathFilter == "" {
return true
}
return
return strings.HasPrefix(path, pathFilter)
}
func (p *chainHop) periodReload(ctx context.Context) error {
+1 -2
View File
@@ -56,7 +56,6 @@ func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.DefaultConfig,
}),
grpc.FailOnNonTempDialError(true),
}
if opts.TLSConfig != nil {
grpcOpts = append(grpcOpts,
@@ -69,7 +68,7 @@ func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
if opts.Token != "" {
grpcOpts = append(grpcOpts, grpc.WithPerRPCCredentials(&rpcCredentials{token: opts.Token}))
}
return grpc.Dial(addr, grpcOpts...)
return grpc.NewClient(addr, grpcOpts...)
}
type rpcCredentials struct {
+5 -1
View File
@@ -251,9 +251,13 @@ func (h *Sniffer) dial(ctx context.Context, conn net.Conn, req *http.Request, ho
}
if ho.Hop != nil {
node = ho.Hop.Select(ctx,
hop.HostSelectOption(host),
hop.ClientIPSelectOption(net.ParseIP(ro.ClientIP)),
hop.ProtocolSelectOption(sniffing.ProtoHTTP),
hop.HostSelectOption(host),
hop.MethodSelectOption(req.Method),
hop.PathSelectOption(req.URL.Path),
hop.QuerySelectOption(req.URL.Query()),
hop.HeaderSelectOption(req.Header),
)
}
if node == nil {
+464
View File
@@ -0,0 +1,464 @@
package routing
import (
"fmt"
"net"
"net/http"
"regexp"
"strings"
"unicode/utf8"
"github.com/go-gost/core/routing"
"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
}
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),
"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),
"HeaderRegexp": expectNParameters(headerRegexp, 1, 2),
"Query": expectNParameters(query, 1, 2),
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
}
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 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[1:]) {
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 PathPrefix 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])
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]
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
}
// 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
}
+141
View File
@@ -0,0 +1,141 @@
package rules
import (
"fmt"
"strings"
"github.com/vulcand/predicate"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
const (
and = "and"
or = "or"
)
type Parser = predicate.Parser
// TreeBuilder defines the type for a Tree builder.
type TreeBuilder func() *Tree
// Tree represents the rules' tree structure.
type Tree struct {
Matcher string
Not bool
Value []string
RuleLeft *Tree
RuleRight *Tree
}
// NewParser constructs a parser for the given matchers.
func NewParser(matchers []string) (predicate.Parser, error) {
parserFuncs := make(map[string]interface{})
for _, matcherName := range matchers {
fn := func(value ...string) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: matcherName,
Value: value,
}
}
}
parserFuncs[matcherName] = fn
parserFuncs[strings.ToLower(matcherName)] = fn
parserFuncs[strings.ToUpper(matcherName)] = fn
parserFuncs[cases.Title(language.Und).String(strings.ToLower(matcherName))] = fn
}
return predicate.NewParser(predicate.Def{
Operators: predicate.Operators{
AND: andFunc,
OR: orFunc,
NOT: notFunc,
},
Functions: parserFuncs,
})
}
func andFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: and,
RuleLeft: left(),
RuleRight: right(),
}
}
}
func orFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: or,
RuleLeft: left(),
RuleRight: right(),
}
}
}
func invert(t *Tree) *Tree {
switch t.Matcher {
case or:
t.Matcher = and
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
case and:
t.Matcher = or
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
default:
t.Not = !t.Not
}
return t
}
func notFunc(elem TreeBuilder) TreeBuilder {
return func() *Tree {
return invert(elem())
}
}
// ParseMatchers returns the subset of matchers in the Tree matching the given matchers.
func (tree *Tree) ParseMatchers(matchers []string) []string {
switch tree.Matcher {
case and, or:
return append(tree.RuleLeft.ParseMatchers(matchers), tree.RuleRight.ParseMatchers(matchers)...)
default:
for _, matcher := range matchers {
if tree.Matcher == matcher {
return lower(tree.Value)
}
}
return nil
}
}
// CheckRule validates the given rule.
func CheckRule(rule *Tree) error {
if len(rule.Value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.Matcher)
}
for _, v := range rule.Value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.Matcher, rule.Value)
}
}
return nil
}
func lower(slice []string) []string {
var lowerStrings []string
for _, value := range slice {
lowerStrings = append(lowerStrings, strings.ToLower(value))
}
return lowerStrings
}
+299
View File
@@ -0,0 +1,299 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTree = Tree + CheckErr
type testTree struct {
Matcher string
Not bool
Value []string
RuleLeft *testTree
RuleRight *testTree
// CheckErr allow knowing if a Tree has a rule error.
CheckErr bool
}
func TestRuleMatch(t *testing.T) {
matchers := []string{"m"}
testCases := []struct {
desc string
rule string
tree testTree
matchers []string
values []string
expectParseErr bool
}{
{
desc: "No rule",
rule: "",
expectParseErr: true,
},
{
desc: "No matcher in rule",
rule: "m",
expectParseErr: true,
},
{
desc: "No value in rule",
rule: "m()",
tree: testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
{
desc: "Empty value in rule",
rule: "m(``)",
tree: testTree{
Matcher: "m",
Value: []string{""},
CheckErr: true,
},
matchers: []string{"m"},
values: []string{""},
},
{
desc: "One value in rule with and",
rule: "m(`1`) &&",
expectParseErr: true,
},
{
desc: "One value in rule with or",
rule: "m(`1`) ||",
expectParseErr: true,
},
{
desc: "One value in rule with missing back tick",
rule: "m(`1)",
expectParseErr: true,
},
{
desc: "One value in rule with missing opening parenthesis",
rule: "m(`1`))",
expectParseErr: true,
},
{
desc: "One value in rule with missing closing parenthesis",
rule: "(m(`1`)",
expectParseErr: true,
},
{
desc: "One value in rule",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "One value in rule with superfluous parenthesis",
rule: "(m(`1`))",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Rule with CAPS matcher",
rule: "M(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Invalid matcher in rule",
rule: "w(`1`)",
expectParseErr: true,
},
{
desc: "Invalid matchers",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"not-m"},
},
{
desc: "Two value in rule",
rule: "m(`1`, `2`)",
tree: testTree{
Matcher: "m",
Value: []string{"1", "2"},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Not one value in rule",
rule: "!m(`1`)",
tree: testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Two value in rule with and",
rule: "m(`1`) && m(`2`)",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or",
rule: "m(`1`) || m(`2`)",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with and negated",
rule: "!(m(`1`) && m(`2`))",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or negated",
rule: "!(m(`1`) || m(`2`))",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "No value in rule",
rule: "m(`1`) && m()",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
matchers: []string{"m"},
values: []string{"1"},
},
}
parser, err := NewParser(matchers)
require.NoError(t, err)
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
parse, err := parser.Parse(test.rule)
if test.expectParseErr {
require.Error(t, err)
return
}
require.NoError(t, err)
treeBuilder, ok := parse.(TreeBuilder)
require.True(t, ok)
tree := treeBuilder()
checkEquivalence(t, &test.tree, tree)
assert.Equal(t, test.values, tree.ParseMatchers(test.matchers))
})
}
}
func checkEquivalence(t *testing.T, expected *testTree, actual *Tree) {
t.Helper()
if actual == nil {
return
}
if actual.RuleLeft != nil {
checkEquivalence(t, expected.RuleLeft, actual.RuleLeft)
}
if actual.RuleRight != nil {
checkEquivalence(t, expected.RuleRight, actual.RuleRight)
}
assert.Equal(t, expected.Matcher, actual.Matcher)
assert.Equal(t, expected.Not, actual.Not)
assert.Equal(t, expected.Value, actual.Value)
t.Logf("%+v -> %v", actual, CheckRule(actual))
if expected.CheckErr {
assert.Error(t, CheckRule(actual))
} else {
assert.NoError(t, CheckRule(actual))
}
}