From 284fb3a98cae2e6e6ca79327988230a3a916996a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 22 May 2019 13:13:39 -0600 Subject: 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. --- modules/caddyhttp/caddyhttp.go | 38 ++++++++++++-------- modules/caddyhttp/matchers.go | 72 +++++++++++++++++++++++++++++--------- modules/caddyhttp/matchers_test.go | 20 +++++------ modules/caddyhttp/routes.go | 64 +++++++++++++++++++++++---------- 4 files changed, 133 insertions(+), 61 deletions(-) diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 2b194cd..1ff2cbc 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -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 diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index eb57156..33300da 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -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..." where +// 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 diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 30b45f6..5e62a90 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -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.*)/baz$", Name: "name"}}, + match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(?P.*)/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"}, diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 07e0566..59f287e 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -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 -- cgit v1.2.3