summaryrefslogtreecommitdiff
path: root/modules/caddyhttp
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-05-22 13:13:39 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-05-22 13:13:39 -0600
commit284fb3a98cae2e6e6ca79327988230a3a916996a (patch)
treeee52b5d7f144cf6510d1689e1ce06887ec213685 /modules/caddyhttp
parentbc00d840e845d42145954839b88f1e836cd51bfd (diff)
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.
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/caddyhttp.go38
-rw-r--r--modules/caddyhttp/matchers.go72
-rw-r--r--modules/caddyhttp/matchers_test.go20
-rw-r--r--modules/caddyhttp/routes.go64
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.<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
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<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"},
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