This is an automated email from the ASF dual-hosted git repository.

alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git


The following commit(s) were added to refs/heads/develop by this push:
     new f498fe5f feat: add support for header based route (#565)
f498fe5f is described below

commit f498fe5f84eca8d20d34ebe187cd6c82e3405a4f
Author: sh2 <[email protected]>
AuthorDate: Sun Jul 2 09:30:33 2023 +0800

    feat: add support for header based route (#565)
    
    * add header route in router
    
    * fix ci
    
    * fix review
    
    * mixup route action and route match into trie
    
    * only implement header based route for now
---
 pixiu/pkg/common/router/router.go      |  50 ++++++++++-
 pixiu/pkg/common/router/router_test.go | 150 +++++++++++++++++++++++++++++++++
 pixiu/pkg/common/router/trie/trie.go   |   2 +-
 pixiu/pkg/model/router.go              |  57 +++++++++++--
 4 files changed, 252 insertions(+), 7 deletions(-)

diff --git a/pixiu/pkg/common/router/router.go 
b/pixiu/pkg/common/router/router.go
index 7148d5a5..c807d3bd 100644
--- a/pixiu/pkg/common/router/router.go
+++ b/pixiu/pkg/common/router/router.go
@@ -18,15 +18,21 @@
 package router
 
 import (
+       stdHttp "net/http"
        "strings"
        "sync"
 )
 
+import (
+       "github.com/pkg/errors"
+)
+
 import (
        "github.com/apache/dubbo-go-pixiu/pixiu/pkg/common/constant"
        "github.com/apache/dubbo-go-pixiu/pixiu/pkg/common/router/trie"
        "github.com/apache/dubbo-go-pixiu/pixiu/pkg/common/util/stringutil"
        "github.com/apache/dubbo-go-pixiu/pixiu/pkg/context/http"
+       "github.com/apache/dubbo-go-pixiu/pixiu/pkg/logger"
        "github.com/apache/dubbo-go-pixiu/pixiu/pkg/model"
        "github.com/apache/dubbo-go-pixiu/pixiu/pkg/server"
 )
@@ -46,6 +52,7 @@ func CreateRouterCoordinator(routeConfig 
*model.RouteConfiguration) *RouterCoord
                server.GetRouterManager().AddRouterListener(rc)
        }
        rc.initTrie()
+       rc.initRegex()
        return rc
 }
 
@@ -54,7 +61,7 @@ func (rm *RouterCoordinator) Route(hc *http.HttpContext) 
(*model.RouteAction, er
        rm.rw.RLock()
        defer rm.rw.RUnlock()
 
-       return rm.activeConfig.Route(hc.Request)
+       return rm.route(hc.Request)
 }
 
 func (rm *RouterCoordinator) RouteByPathAndName(path, method string) 
(*model.RouteAction, error) {
@@ -64,6 +71,31 @@ func (rm *RouterCoordinator) RouteByPathAndName(path, method 
string) (*model.Rou
        return rm.activeConfig.RouteByPathAndMethod(path, method)
 }
 
+func (rm *RouterCoordinator) route(req *stdHttp.Request) (*model.RouteAction, 
error) {
+       // match those route that only contains headers first
+       var matched []*model.Router
+       for _, route := range rm.activeConfig.Routes {
+               if len(route.Match.Prefix) > 0 {
+                       continue
+               }
+               if route.Match.MatchHeader(req) {
+                       matched = append(matched, route)
+               }
+       }
+
+       // always return the first match of header if got any
+       if len(matched) > 0 {
+               if len(matched[0].Route.Cluster) == 0 {
+                       return nil, errors.New("action is nil. please check 
your configuration.")
+               }
+               return &matched[0].Route, nil
+       }
+
+       // match those route that only contains prefix
+       // TODO: may consider implementing both prefix and header in the future
+       return rm.activeConfig.Route(req)
+}
+
 func getTrieKey(method string, path string, isPrefix bool) string {
        if isPrefix {
                if !strings.HasSuffix(path, constant.PathSlash) {
@@ -83,6 +115,22 @@ func (rm *RouterCoordinator) initTrie() {
        }
 }
 
+func (rm *RouterCoordinator) initRegex() {
+       for _, router := range rm.activeConfig.Routes {
+               headers := router.Match.Headers
+               for i := range headers {
+                       if headers[i].Regex && len(headers[i].Values) > 0 {
+                               // regexp always use first value of header
+                               err := 
headers[i].SetValueRegex(headers[i].Values[0])
+                               if err != nil {
+                                       logger.Errorf("invalid regexp in 
headers[%d]: %v", i, err)
+                                       panic(err)
+                               }
+                       }
+               }
+       }
+}
+
 // OnAddRouter add router
 func (rm *RouterCoordinator) OnAddRouter(r *model.Router) {
        //TODO: lock move to trie node
diff --git a/pixiu/pkg/common/router/router_test.go 
b/pixiu/pkg/common/router/router_test.go
index 27c35853..b41a0b5b 100644
--- a/pixiu/pkg/common/router/router_test.go
+++ b/pixiu/pkg/common/router/router_test.go
@@ -74,3 +74,153 @@ func TestCreateRouterCoordinator(t *testing.T) {
        r.OnAddRouter(router)
        r.OnDeleteRouter(router)
 }
+
+func TestRoute(t *testing.T) {
+       const (
+               Cluster1 = "test-cluster-1"
+       )
+
+       hcmc := model.HttpConnectionManagerConfig{
+               RouteConfig: model.RouteConfiguration{
+                       Routes: []*model.Router{
+                               {
+                                       ID: "1",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
+                                                       {
+                                                               Name:   "A",
+                                                               Values: 
[]string{"1", "2", "3"},
+                                                       },
+                                                       {
+                                                               Name:   "A",
+                                                               Values: 
[]string{"3", "4", "5"},
+                                                       },
+                                                       {
+                                                               Name:   "B",
+                                                               Values: 
[]string{"1"},
+                                                       },
+                                                       {
+                                                               Name:   
"normal-regex",
+                                                               Values: 
[]string{"(k){2}"},
+                                                               Regex:  true,
+                                                       },
+                                                       {
+                                                               Name:   
"broken-regex",
+                                                               Values: 
[]string{"(t){2]]"},
+                                                               Regex:  true,
+                                                       },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster1,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                       },
+                       Dynamic: false,
+               },
+               HTTPFilters: []*model.HTTPFilter{
+                       {
+                               Name:   "test",
+                               Config: nil,
+                       },
+               },
+               ServerName:        "test_http_dubbo",
+               GenerateRequestID: false,
+               IdleTimeoutStr:    "100",
+       }
+
+       testCases := []struct {
+               Name   string
+               URL    string
+               Method string
+               Header map[string]string
+               Expect string
+       }{
+               {
+                       Name: "one override header",
+                       URL:  "/user",
+                       Header: map[string]string{
+                               "A": "1",
+                       },
+                       Expect: "test-cluster-1",
+               },
+               {
+                       Name: "one header matched",
+                       URL:  "/user",
+                       Header: map[string]string{
+                               "A": "3",
+                       },
+                       Expect: Cluster1,
+               },
+               {
+                       Name: "more header with one regex matched",
+                       URL:  "/user",
+                       Header: map[string]string{
+                               "A":            "5",
+                               "normal-regex": "kkkk",
+                       },
+                       Expect: Cluster1,
+               },
+               {
+                       Name:   "one header but wrong method",
+                       URL:    "/user",
+                       Method: "PUT",
+                       Header: map[string]string{
+                               "A": "3",
+                       },
+                       Expect: "route failed for PUT/user, no rules matched.",
+               },
+               {
+                       Name: "one broken regex header",
+                       URL:  "/user",
+                       Header: map[string]string{
+                               "broken-regex": "tt",
+                       },
+                       Expect: "route failed for GET/user, no rules matched.",
+               },
+               {
+                       Name: "one matched header 2",
+                       Header: map[string]string{
+                               "B": "1",
+                       },
+                       Expect: Cluster1,
+               },
+               {
+                       Name:   "only header but wrong method",
+                       Method: "DELETE",
+                       Header: map[string]string{
+                               "B": "1",
+                       },
+                       Expect: "route failed for DELETE, no rules matched.",
+               },
+       }
+
+       r := CreateRouterCoordinator(&hcmc.RouteConfig)
+
+       for _, tc := range testCases {
+               t.Run(tc.Name, func(t *testing.T) {
+                       method := "GET"
+                       if len(tc.Method) > 0 {
+                               method = tc.Method
+                       }
+                       request, err := http.NewRequest(method, tc.URL, nil)
+                       assert.NoError(t, err)
+
+                       if tc.Header != nil {
+                               for k, v := range tc.Header {
+                                       request.Header.Set(k, v)
+                               }
+                       }
+                       c := mock.GetMockHTTPContext(request)
+
+                       a, err := r.Route(c)
+                       if err != nil {
+                               assert.Equal(t, tc.Expect, err.Error())
+                       } else {
+                               assert.Equal(t, tc.Expect, a.Cluster)
+                       }
+               })
+       }
+}
diff --git a/pixiu/pkg/common/router/trie/trie.go 
b/pixiu/pkg/common/router/trie/trie.go
index b422e9b0..bb533800 100644
--- a/pixiu/pkg/common/router/trie/trie.go
+++ b/pixiu/pkg/common/router/trie/trie.go
@@ -49,7 +49,7 @@ func NewTrieWithDefault(path string, defVal interface{}) Trie 
{
 
 // Node
 type Node struct {
-       matchStr         string           //abc match abc, :a match all words 
as a variable names a , * match all words  ,** match all words and children.
+       matchStr         string           // abc match abc, :a match all words 
as a variable names a , * match all words  ,** match all words and children.
        children         map[string]*Node // in path /a/b/c  , b is child of a 
, c is child of b
        PathVariablesSet map[string]*Node // in path /:a/b/c/:d , :a is a path 
variable node of level1 , :d is path variable node of level4
        PathVariableNode *Node            // in path /:a/b/c/:d , /b/c/:d is a 
child tree of pathVariable node :a ,and some special logic for match 
pathVariable it better not store in children.
diff --git a/pixiu/pkg/model/router.go b/pixiu/pkg/model/router.go
index 7730a23b..ce41669d 100644
--- a/pixiu/pkg/model/router.go
+++ b/pixiu/pkg/model/router.go
@@ -44,8 +44,8 @@ type (
                Prefix string `yaml:"prefix" json:"prefix" 
mapstructure:"prefix"`
                Path   string `yaml:"path" json:"path" mapstructure:"path"`
                // Regex   string          `yaml:"regex" json:"regex" 
mapstructure:"regex"` TODO: next version
-               Methods []string `yaml:"methods" json:"methods" 
mapstructure:"methods"`
-               // Headers []HeaderMatcher `yaml:"headers" json:"headers" 
mapstructure:"headers"`
+               Methods []string        `yaml:"methods" json:"methods" 
mapstructure:"methods"`
+               Headers []HeaderMatcher `yaml:"headers,omitempty" 
json:"headers,omitempty" mapstructure:"headers"`
                // pathRE  *regexp.Regexp
        }
 
@@ -62,7 +62,7 @@ type (
                Dynamic   bool      `yaml:"dynamic" json:"dynamic" 
mapstructure:"dynamic"`
        }
 
-       // Name header key, Value header value, Regex header value is regex
+       // HeaderMatcher include Name header key, Values header value, Regex 
regex value
        HeaderMatcher struct {
                Name    string   `yaml:"name" json:"name" mapstructure:"name"`
                Values  []string `yaml:"values" json:"values" 
mapstructure:"values"`
@@ -82,15 +82,62 @@ func (rc *RouteConfiguration) RouteByPathAndMethod(path, 
method string) (*RouteA
 
        node, _, _ := rc.RouteTrie.Match(stringutil.GetTrieKey(method, path))
        if node == nil {
-               return nil, errors.Errorf("route failed for %s,no rules 
matched.", stringutil.GetTrieKey(method, path))
+               return nil, errors.Errorf("route failed for %s, no rules 
matched.", stringutil.GetTrieKey(method, path))
        }
        if node.GetBizInfo() == nil {
-               return nil, errors.Errorf("info is nil.please check your 
configuration.")
+               return nil, errors.Errorf("action is nil. please check your 
configuration.")
        }
        ret := (node.GetBizInfo()).(RouteAction)
+
        return &ret, nil
 }
 
 func (rc *RouteConfiguration) Route(req *stdHttp.Request) (*RouteAction, 
error) {
        return rc.RouteByPathAndMethod(req.URL.Path, req.Method)
 }
+
+// MatchHeader used when there's only headers to match
+func (rm *RouterMatch) MatchHeader(req *stdHttp.Request) bool {
+       if len(rm.Methods) > 0 {
+               for _, method := range rm.Methods {
+                       if method == req.Method {
+                               goto HEADER
+                       }
+               }
+               return false
+       }
+HEADER:
+       for _, header := range rm.Headers {
+               if val := req.Header.Get(header.Name); len(val) > 0 {
+                       if header.MatchValues(val) {
+                               return true
+                       }
+               }
+       }
+       return false
+}
+
+// MatchValues match values in header, including regex type
+func (hm *HeaderMatcher) MatchValues(dst string) bool {
+       if hm.Regex && hm.valueRE != nil {
+               return hm.valueRE.MatchString(dst)
+       }
+
+       for _, src := range hm.Values {
+               if src == dst {
+                       return true
+               }
+       }
+       return false
+}
+
+// SetValueRegex compile the regex, disable regex if it failed
+func (hm *HeaderMatcher) SetValueRegex(regex string) error {
+       r, err := regexp.Compile(regex)
+       if err == nil {
+               hm.valueRE = r
+               return nil
+       }
+       hm.Regex = false
+       return err
+}

Reply via email to