Skip to content

Commit

Permalink
support routing by query string (#823)
Browse files Browse the repository at this point in the history
* support Routing by Query String

* style update and regexp empty check
  • Loading branch information
nullsimon committed Oct 12, 2022
1 parent 42ae49c commit df7c761
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 6 deletions.
44 changes: 38 additions & 6 deletions pkg/object/httpserver/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type (
headers []*Header
clientMaxBodySize int64
matchAllHeader bool
queries []*Query
}

route struct {
Expand Down Expand Up @@ -219,6 +220,10 @@ func newMuxPath(parentIPFilters *ipfilter.IPFilters, path *Path) *MuxPath {
p.initHeaderRoute()
}

for _, q := range path.Queries {
q.initQueryRoute()
}

return &MuxPath{
ipFilter: newIPFilter(path.IPFilter),
ipFilterChain: newIPFilterChain(parentIPFilters, path.IPFilter),
Expand All @@ -233,6 +238,7 @@ func newMuxPath(parentIPFilters *ipfilter.IPFilters, path *Path) *MuxPath {
headers: path.Headers,
clientMaxBodySize: path.ClientMaxBodySize,
matchAllHeader: path.MatchAllHeader,
queries: path.Queries,
}
}

Expand Down Expand Up @@ -314,6 +320,25 @@ func (mp *MuxPath) matchHeaders(r *httpprot.Request) bool {
return mp.matchAllHeader
}

func (mp *MuxPath) matchQueries(r *httpprot.Request) bool {
if len(mp.queries) == 0 {
return true
}
query := r.URL().Query()
for _, q := range mp.queries {
v := query.Get(q.Key)
if len(q.Values) > 0 && !stringtool.StrInSlice(v, q.Values) {
return false
}

if q.Regexp != "" && !q.re.MatchString(v) {
return false
}
}

return true
}

func newMux(httpStat *httpstat.HTTPStat, topN *httpstat.TopN, mapper context.MuxMapper) *mux {
m := &mux{
httpStat: httpStat,
Expand Down Expand Up @@ -544,13 +569,13 @@ func (mi *muxInstance) serveHTTP(stdw http.ResponseWriter, stdr *http.Request) {
}

func (mi *muxInstance) search(req *httpprot.Request) *route {
headerMismatch, methodMismatch := false, false
headerMismatch, methodMismatch, queryMismatch := false, false, false

ip := req.RealIP()

// The key of the cache is req.Host + req.Method + req.URL.Path,
// and if a path is cached, we are sure it does not contain any
// headers.
// headers or any queries.
r := mi.getRouteFromCache(req)
if r != nil {
if r.code != 0 {
Expand Down Expand Up @@ -588,15 +613,22 @@ func (mi *muxInstance) search(req *httpprot.Request) *route {
continue
}

// The path can be put into the cache if it has no headers.
if len(path.headers) == 0 {
// only if headers and query are empty, we can cache the result.
if len(path.headers) == 0 && len(path.queries) == 0 {
r = &route{code: 0, path: path}
mi.putRouteToCache(req, r)
} else if !path.matchHeaders(req) {
}

if len(path.headers) > 0 && !path.matchHeaders(req) {
headerMismatch = true
continue
}

if len(path.queries) > 0 && !path.matchQueries(req) {
queryMismatch = true
continue
}

if !allowIP(path.ipFilter, ip) {
return forbidden
}
Expand All @@ -605,7 +637,7 @@ func (mi *muxInstance) search(req *httpprot.Request) *route {
}
}

if headerMismatch {
if headerMismatch || queryMismatch {
return badRequest
}

Expand Down
151 changes: 151 additions & 0 deletions pkg/object/httpserver/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"testing/iotest"
Expand Down Expand Up @@ -180,6 +181,26 @@ func TestMuxPath(t *testing.T) {
assert.NotNil(mp)
mp.rewrite(req)
assert.Equal("/1abz", req.Path())

// 5. match query
stdr.URL.RawQuery = "q=v1&q=v2"
mp = newMuxPath(nil, &Path{Queries: []*Query{{
Key: "q",
Values: []string{"v1", "v2"},
}}})
assert.True(mp.matchQueries(req))

mp = newMuxPath(nil, &Path{Queries: []*Query{{
Key: "q",
Regexp: "v[0-9]",
}}})
assert.True(mp.matchQueries(req))

mp = newMuxPath(nil, &Path{Queries: []*Query{{
Key: "q2",
Values: []string{"v1", "v2"},
}}})
assert.False(mp.matchQueries(req))
}

func TestMuxReload(t *testing.T) {
Expand Down Expand Up @@ -401,6 +422,40 @@ rules:
values: ["true"]
matchAllHeader: true
backend: 123-pipeline
- path: /queryParams
methods: [GET]
queries:
- key: "q"
values: ["v1", "v2"]
backend: 123-pipeline
- path: /queryParamsMultiKey
methods: [GET]
queries:
- key: "q"
values: ["v1", "v2"]
- key: "q2"
values: ["v3", "v4"]
backend: 123-pipeline
- path: /queryParamsRegexp
methods: [GET]
queries:
- key: "q2"
regexp: "^v[0-9]$"
backend: 123-pipeline
- path: /queryParamsRegexpAndValues
methods: [GET]
queries:
- key: "q3"
values: ["v1", "v2"]
regexp: "^v[0-9]$"
backend: 123-pipeline
- path: /queryParamsRegexpAndValues2
methods: [GET]
queries:
- key: "id"
values: ["011"]
regexp: "[0-9]+"
backend: 123-pipeline
`

superSpec, err := supervisor.NewSpec(yamlConfig)
Expand Down Expand Up @@ -486,4 +541,100 @@ rules:
stdr.Header.Set("AllMatch", "false")
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string single key
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParams", http.NoBody)
v := url.Values{"q": []string{"v1"}}
stdr.URL.RawQuery = v.Encode()
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string single key
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParams", http.NoBody)
v = url.Values{"q": []string{"v1", "v2"}}
stdr.URL.RawQuery = v.Encode()
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string single key
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParams", http.NoBody)
stdr.URL.RawQuery = "q=v1"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string multi key
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsMultiKey", http.NoBody)
v = url.Values{"q": []string{"v1", "v3"}, "q2": []string{"v6"}}
stdr.URL.RawQuery = v.Encode()
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string multi key
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsMultiKey", http.NoBody)
v = url.Values{"q": []string{"v1", "v3"}}
stdr.URL.RawQuery = v.Encode()
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string multi key
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsMultiKey", http.NoBody)
v = url.Values{"q": []string{"v1", "v3"}, "q2": []string{"v3"}}
stdr.URL.RawQuery = v.Encode()
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexp", http.NoBody)
stdr.URL.RawQuery = "q2=v1"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexp", http.NoBody)
stdr.URL.RawQuery = "q2=vv"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues", http.NoBody)
stdr.URL.RawQuery = "q3=v2"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues", http.NoBody)
stdr.URL.RawQuery = "q3=v1&q3=v4"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues", http.NoBody)
stdr.URL.RawQuery = "q3=v4"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues", http.NoBody)
stdr.URL.RawQuery = "q3=v4"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues2", http.NoBody)
stdr.URL.RawQuery = "id=011&&id=baz"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(0, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues2", http.NoBody)
stdr.URL.RawQuery = "id=baz&&id=011"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

// query string values and regexp
stdr, _ = http.NewRequest(http.MethodGet, "http:https://www.megaease.com/queryParamsRegexpAndValues2", http.NoBody)
stdr.URL.RawQuery = "id=baz"
req, _ = httpprot.NewRequest(stdr)
assert.Equal(400, mi.search(req).code)

}
24 changes: 24 additions & 0 deletions pkg/object/httpserver/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type (
Headers []*Header `json:"headers" jsonschema:"omitempty"`
ClientMaxBodySize int64 `json:"clientMaxBodySize" jsonschema:"omitempty"`
MatchAllHeader bool `json:"matchAllHeader" jsonschema:"omitempty"`
Queries []*Query `json:"queries,omitempty" jsonschema:"omitempty"`
}

// Header is the third level entry of router. A header entry is always under a specific path entry, that is to mean
Expand All @@ -101,6 +102,15 @@ type (

headerRE *regexp.Regexp
}

// Query is the third level entry
Query struct {
Key string `json:"key" jsonschema:"required"`
Values []string `json:"values,omitempty" jsonschema:"omitempty,uniqueItems=true"`
Regexp string `json:"regexp,omitempty" jsonschema:"omitempty,format=regexp"`

re *regexp.Regexp
}
)

// Validate validates HTTPServerSpec.
Expand Down Expand Up @@ -211,3 +221,17 @@ func (p *Path) Validate() error {

return nil
}

func (q *Query) initQueryRoute() {
if q.Regexp != "" {
q.re = regexp.MustCompile(q.Regexp)
}
}

func (q *Query) Validate() error {
if len(q.Values) == 0 && q.Regexp == "" {
return fmt.Errorf("both of values and regexp are empty for key: %s", q.Key)
}

return nil
}

0 comments on commit df7c761

Please sign in to comment.