Allow multiple matcher sets in routes (OR'ed together)

Also export MatchRegexp in case other matcher modules find it useful.
Add comments to the exported matchers.
This commit is contained in:
Matthew Holt 2019-05-22 13:13:39 -06:00
parent bc00d840e8
commit 284fb3a98c
4 changed files with 133 additions and 61 deletions

View file

@ -53,13 +53,17 @@ func (app *App) Provision(ctx caddy2.Context) error {
for i := range srv.Listen {
srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
}
err := srv.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up server routes: %v", err)
if srv.Routes != nil {
err := srv.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up server routes: %v", err)
}
}
err = srv.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up server error handling routes: %v", err)
if srv.Errors != nil {
err := srv.Errors.Routes.Provision(ctx)
if err != nil {
return fmt.Errorf("setting up server error handling routes: %v", err)
}
}
}
@ -187,13 +191,15 @@ func (app *App) automaticHTTPS() error {
// find all qualifying domain names, de-duplicated
domainSet := make(map[string]struct{})
for _, route := range srv.Routes {
for _, m := range route.matchers {
if hm, ok := m.(*MatchHost); ok {
for _, d := range *hm {
if !certmagic.HostQualifies(d) {
continue
for _, matcherSet := range route.matcherSets {
for _, m := range matcherSet {
if hm, ok := m.(*MatchHost); ok {
for _, d := range *hm {
if !certmagic.HostQualifies(d) {
continue
}
domainSet[d] = struct{}{}
}
domainSet[d] = struct{}{}
}
}
}
@ -245,9 +251,11 @@ func (app *App) automaticHTTPS() error {
redirTo += "{http.request.uri}"
redirRoutes = append(redirRoutes, ServerRoute{
matchers: []RequestMatcher{
MatchProtocol("http"),
MatchHost(domains),
matcherSets: []MatcherSet{
{
MatchProtocol("http"),
MatchHost(domains),
},
},
responder: Static{
StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead

View file

@ -17,16 +17,35 @@ import (
)
type (
MatchHost []string
MatchPath []string
MatchPathRE struct{ matchRegexp }
MatchMethod []string
MatchQuery url.Values
MatchHeader http.Header
MatchHeaderRE map[string]*matchRegexp
MatchProtocol string
// MatchHost matches requests by the Host value.
MatchHost []string
// MatchPath matches requests by the URI's path.
MatchPath []string
// MatchPathRE matches requests by a regular expression on the URI's path.
MatchPathRE struct{ MatchRegexp }
// MatchMethod matches requests by the method.
MatchMethod []string
// MatchQuery matches requests by URI's query string.
MatchQuery url.Values
// MatchHeader matches requests by header fields.
MatchHeader http.Header
// MatchHeaderRE matches requests by a regular expression on header fields.
MatchHeaderRE map[string]*MatchRegexp
// MatchProtocol matches requests by protocol.
MatchProtocol string
// MatchStarlarkExpr matches requests by evaluating a Starlark expression.
MatchStarlarkExpr string
MatchTable string // TODO: finish implementing
// MatchTable matches requests by values in the table.
MatchTable string // TODO: finish implementing
)
func init() {
@ -68,6 +87,7 @@ func init() {
})
}
// Match returns true if r matches m.
func (m MatchHost) Match(r *http.Request) bool {
outer:
for _, host := range m {
@ -93,6 +113,7 @@ outer:
return false
}
// Match returns true if r matches m.
func (m MatchPath) Match(r *http.Request) bool {
for _, matchPath := range m {
compare := r.URL.Path
@ -111,11 +132,13 @@ func (m MatchPath) Match(r *http.Request) bool {
return false
}
// Match returns true if r matches m.
func (m MatchPathRE) Match(r *http.Request) bool {
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
return m.match(r.URL.Path, repl, "path_regexp")
return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp")
}
// Match returns true if r matches m.
func (m MatchMethod) Match(r *http.Request) bool {
for _, method := range m {
if r.Method == method {
@ -125,6 +148,7 @@ func (m MatchMethod) Match(r *http.Request) bool {
return false
}
// Match returns true if r matches m.
func (m MatchQuery) Match(r *http.Request) bool {
for param, vals := range m {
paramVal := r.URL.Query().Get(param)
@ -137,6 +161,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
return false
}
// Match returns true if r matches m.
func (m MatchHeader) Match(r *http.Request) bool {
for field, allowedFieldVals := range m {
var match bool
@ -157,10 +182,11 @@ func (m MatchHeader) Match(r *http.Request) bool {
return true
}
// Match returns true if r matches m.
func (m MatchHeaderRE) Match(r *http.Request) bool {
for field, rm := range m {
repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
match := rm.match(r.Header.Get(field), repl, "header_regexp")
match := rm.Match(r.Header.Get(field), repl, "header_regexp")
if !match {
return false
}
@ -168,6 +194,7 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
return true
}
// Provision compiles m's regular expressions.
func (m MatchHeaderRE) Provision() error {
for _, rm := range m {
err := rm.Provision()
@ -178,6 +205,7 @@ func (m MatchHeaderRE) Provision() error {
return nil
}
// Validate validates m's regular expressions.
func (m MatchHeaderRE) Validate() error {
for _, rm := range m {
err := rm.Validate()
@ -188,6 +216,7 @@ func (m MatchHeaderRE) Validate() error {
return nil
}
// Match returns true if r matches m.
func (m MatchProtocol) Match(r *http.Request) bool {
switch string(m) {
case "grpc":
@ -200,6 +229,7 @@ func (m MatchProtocol) Match(r *http.Request) bool {
return false
}
// Match returns true if r matches m.
func (m MatchStarlarkExpr) Match(r *http.Request) bool {
input := string(m)
thread := new(starlark.Thread)
@ -213,15 +243,16 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool {
return val.String() == "True"
}
// matchRegexp is just the fields common among
// matchers that can use regular expressions.
type matchRegexp struct {
// MatchRegexp is an embeddable type for matching
// using regular expressions.
type MatchRegexp struct {
Name string `json:"name"`
Pattern string `json:"pattern"`
compiled *regexp.Regexp
}
func (mre *matchRegexp) Provision() error {
// Provision compiles the regular expression.
func (mre *MatchRegexp) Provision() error {
re, err := regexp.Compile(mre.Pattern)
if err != nil {
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
@ -230,14 +261,21 @@ func (mre *matchRegexp) Provision() error {
return nil
}
func (mre *matchRegexp) Validate() error {
// Validate ensures mre is set up correctly.
func (mre *MatchRegexp) Validate() error {
if mre.Name != "" && !wordRE.MatchString(mre.Name) {
return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
}
return nil
}
func (mre *matchRegexp) match(input string, repl caddy2.Replacer, scope string) bool {
// Match returns true if input matches the compiled regular
// expression in mre. It sets values on the replacer repl
// associated with capture groups, using the given scope
// (namespace). Capture groups stored to repl will take on
// the name "http.matchers.<scope>.<mre.Name>.<N>" where
// <N> is the name or number of the capture group.
func (mre *MatchRegexp) Match(input string, repl caddy2.Replacer, scope string) bool {
matches := mre.compiled.FindStringSubmatch(input)
if matches == nil {
return false

View file

@ -176,38 +176,38 @@ func TestPathREMatcher(t *testing.T) {
expect: true,
},
{
match: MatchPathRE{matchRegexp{Pattern: "/"}},
match: MatchPathRE{MatchRegexp{Pattern: "/"}},
input: "/",
expect: true,
},
{
match: MatchPathRE{matchRegexp{Pattern: "/foo"}},
match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
input: "/foo",
expect: true,
},
{
match: MatchPathRE{matchRegexp{Pattern: "/foo"}},
match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
input: "/foo/",
expect: true,
},
{
match: MatchPathRE{matchRegexp{Pattern: "/bar"}},
match: MatchPathRE{MatchRegexp{Pattern: "/bar"}},
input: "/foo/",
expect: false,
},
{
match: MatchPathRE{matchRegexp{Pattern: "^/bar"}},
match: MatchPathRE{MatchRegexp{Pattern: "^/bar"}},
input: "/foo/bar",
expect: false,
},
{
match: MatchPathRE{matchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
input: "/foo/bar/baz",
expect: true,
expectRepl: map[string]string{"name.1": "bar"},
},
{
match: MatchPathRE{matchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
input: "/foo/bar/baz",
expect: true,
expectRepl: map[string]string{"name.myparam": "bar"},
@ -315,17 +315,17 @@ func TestHeaderREMatcher(t *testing.T) {
expectRepl map[string]string
}{
{
match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "foo"}},
match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "foo"}},
input: http.Header{"Field": []string{"foo"}},
expect: true,
},
{
match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "$foo^"}},
match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "$foo^"}},
input: http.Header{"Field": []string{"foobar"}},
expect: false,
},
{
match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
input: http.Header{"Field": []string{"foobar"}},
expect: true,
expectRepl: map[string]string{"name.1": "bar"},

View file

@ -12,17 +12,42 @@ import (
// middlewares, and a responder for handling HTTP
// requests.
type ServerRoute struct {
Group string `json:"group,omitempty"`
Matchers map[string]json.RawMessage `json:"match,omitempty"`
Apply []json.RawMessage `json:"apply,omitempty"`
Respond json.RawMessage `json:"respond,omitempty"`
Group string `json:"group,omitempty"`
MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
Apply []json.RawMessage `json:"apply,omitempty"`
Respond json.RawMessage `json:"respond,omitempty"`
Terminal bool `json:"terminal,omitempty"`
// decoded values
matchers []RequestMatcher
middleware []MiddlewareHandler
responder Handler
matcherSets []MatcherSet
middleware []MiddlewareHandler
responder Handler
}
func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool {
for _, ms := range sr.matcherSets {
if ms.Match(r) {
return true
}
}
return false
}
// MatcherSet is a set of matchers which
// must all match in order for the request
// to be matched successfully.
type MatcherSet []RequestMatcher
// Match returns true if the request matches all
// matchers in mset.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
if !m.Match(r) {
return false
}
}
return true
}
// RouteList is a list of server routes that can
@ -33,14 +58,18 @@ type RouteList []ServerRoute
func (routes RouteList) Provision(ctx caddy2.Context) error {
for i, route := range routes {
// matchers
for modName, rawMsg := range route.Matchers {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
for _, matcherSet := range route.MatcherSets {
var matchers MatcherSet
for modName, rawMsg := range matcherSet {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
matchers = append(matchers, val.(RequestMatcher))
}
routes[i].matchers = append(routes[i].matchers, val.(RequestMatcher))
routes[i].matcherSets = append(routes[i].matcherSets, matchers)
}
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help?
// middleware
for j, rawMsg := range route.Apply {
@ -78,13 +107,10 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re
var responder Handler
groups := make(map[string]struct{})
routeLoop:
for _, route := range routes {
// see if route matches
for _, m := range route.matchers {
if !m.Match(req) {
continue routeLoop
}
// route must match at least one of the matcher sets
if !route.anyMatcherSetMatches(req) {
continue
}
// if route is part of a group, ensure only