@marcel91200 / caddy Mirror · read-only / commits / 284fb3a9
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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.

Matthew Holt committed May 22, 2019 at 13:13 UTC 284fb3a98cae2e6e6ca79327988230a3a916996a
4 files changed +133 -61
modules/caddyhttp/caddyhttp.go
+23 -15
@@ -53,13 +53,17 @@ func (app *App) Provision(ctx caddy2.Context) error {
53 for i := range srv.Listen {
54 srv.Listen[i] = repl.ReplaceAll(srv.Listen[i], "")
55 }
56 - err := srv.Routes.Provision(ctx)
57 - if err != nil {
58 - return fmt.Errorf("setting up server routes: %v", err)
56 + if srv.Routes != nil {
57 + err := srv.Routes.Provision(ctx)
58 + if err != nil {
59 + return fmt.Errorf("setting up server routes: %v", err)
60 + }
61 }
60 - err = srv.Errors.Routes.Provision(ctx)
61 - if err != nil {
62 - return fmt.Errorf("setting up server error handling routes: %v", err)
62 + if srv.Errors != nil {
63 + err := srv.Errors.Routes.Provision(ctx)
64 + if err != nil {
65 + return fmt.Errorf("setting up server error handling routes: %v", err)
66 + }
67 }
68 }
69
@@ -187,13 +191,15 @@ func (app *App) automaticHTTPS() error {
191 // find all qualifying domain names, de-duplicated
192 domainSet := make(map[string]struct{})
193 for _, route := range srv.Routes {
190 - for _, m := range route.matchers {
191 - if hm, ok := m.(*MatchHost); ok {
192 - for _, d := range *hm {
193 - if !certmagic.HostQualifies(d) {
194 - continue
194 + for _, matcherSet := range route.matcherSets {
195 + for _, m := range matcherSet {
196 + if hm, ok := m.(*MatchHost); ok {
197 + for _, d := range *hm {
198 + if !certmagic.HostQualifies(d) {
199 + continue
200 + }
201 + domainSet[d] = struct{}{}
202 }
196 - domainSet[d] = struct{}{}
203 }
204 }
205 }
@@ -245,9 +251,11 @@ func (app *App) automaticHTTPS() error {
251 redirTo += "{http.request.uri}"
252
253 redirRoutes = append(redirRoutes, ServerRoute{
248 - matchers: []RequestMatcher{
249 - MatchProtocol("http"),
250 - MatchHost(domains),
254 + matcherSets: []MatcherSet{
255 + {
256 + MatchProtocol("http"),
257 + MatchHost(domains),
258 + },
259 },
260 responder: Static{
261 StatusCode: http.StatusTemporaryRedirect, // TODO: use permanent redirect instead
modules/caddyhttp/matchers.go
+55 -17
@@ -17,16 +17,35 @@ import (
17 )
18
19 type (
20 - MatchHost []string
21 - MatchPath []string
22 - MatchPathRE struct{ matchRegexp }
23 - MatchMethod []string
24 - MatchQuery url.Values
25 - MatchHeader http.Header
26 - MatchHeaderRE map[string]*matchRegexp
27 - MatchProtocol string
20 + // MatchHost matches requests by the Host value.
21 + MatchHost []string
22 +
23 + // MatchPath matches requests by the URI's path.
24 + MatchPath []string
25 +
26 + // MatchPathRE matches requests by a regular expression on the URI's path.
27 + MatchPathRE struct{ MatchRegexp }
28 +
29 + // MatchMethod matches requests by the method.
30 + MatchMethod []string
31 +
32 + // MatchQuery matches requests by URI's query string.
33 + MatchQuery url.Values
34 +
35 + // MatchHeader matches requests by header fields.
36 + MatchHeader http.Header
37 +
38 + // MatchHeaderRE matches requests by a regular expression on header fields.
39 + MatchHeaderRE map[string]*MatchRegexp
40 +
41 + // MatchProtocol matches requests by protocol.
42 + MatchProtocol string
43 +
44 + // MatchStarlarkExpr matches requests by evaluating a Starlark expression.
45 MatchStarlarkExpr string
29 - MatchTable string // TODO: finish implementing
46 +
47 + // MatchTable matches requests by values in the table.
48 + MatchTable string // TODO: finish implementing
49 )
50
51 func init() {
@@ -68,6 +87,7 @@ func init() {
87 })
88 }
89
90 +// Match returns true if r matches m.
91 func (m MatchHost) Match(r *http.Request) bool {
92 outer:
93 for _, host := range m {
@@ -93,6 +113,7 @@ outer:
113 return false
114 }
115
116 +// Match returns true if r matches m.
117 func (m MatchPath) Match(r *http.Request) bool {
118 for _, matchPath := range m {
119 compare := r.URL.Path
@@ -111,11 +132,13 @@ func (m MatchPath) Match(r *http.Request) bool {
132 return false
133 }
134
135 +// Match returns true if r matches m.
136 func (m MatchPathRE) Match(r *http.Request) bool {
137 repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
116 - return m.match(r.URL.Path, repl, "path_regexp")
138 + return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp")
139 }
140
141 +// Match returns true if r matches m.
142 func (m MatchMethod) Match(r *http.Request) bool {
143 for _, method := range m {
144 if r.Method == method {
@@ -125,6 +148,7 @@ func (m MatchMethod) Match(r *http.Request) bool {
148 return false
149 }
150
151 +// Match returns true if r matches m.
152 func (m MatchQuery) Match(r *http.Request) bool {
153 for param, vals := range m {
154 paramVal := r.URL.Query().Get(param)
@@ -137,6 +161,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
161 return false
162 }
163
164 +// Match returns true if r matches m.
165 func (m MatchHeader) Match(r *http.Request) bool {
166 for field, allowedFieldVals := range m {
167 var match bool
@@ -157,10 +182,11 @@ func (m MatchHeader) Match(r *http.Request) bool {
182 return true
183 }
184
185 +// Match returns true if r matches m.
186 func (m MatchHeaderRE) Match(r *http.Request) bool {
187 for field, rm := range m {
188 repl := r.Context().Value(caddy2.ReplacerCtxKey).(caddy2.Replacer)
163 - match := rm.match(r.Header.Get(field), repl, "header_regexp")
189 + match := rm.Match(r.Header.Get(field), repl, "header_regexp")
190 if !match {
191 return false
192 }
@@ -168,6 +194,7 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
194 return true
195 }
196
197 +// Provision compiles m's regular expressions.
198 func (m MatchHeaderRE) Provision() error {
199 for _, rm := range m {
200 err := rm.Provision()
@@ -178,6 +205,7 @@ func (m MatchHeaderRE) Provision() error {
205 return nil
206 }
207
208 +// Validate validates m's regular expressions.
209 func (m MatchHeaderRE) Validate() error {
210 for _, rm := range m {
211 err := rm.Validate()
@@ -188,6 +216,7 @@ func (m MatchHeaderRE) Validate() error {
216 return nil
217 }
218
219 +// Match returns true if r matches m.
220 func (m MatchProtocol) Match(r *http.Request) bool {
221 switch string(m) {
222 case "grpc":
@@ -200,6 +229,7 @@ func (m MatchProtocol) Match(r *http.Request) bool {
229 return false
230 }
231
232 +// Match returns true if r matches m.
233 func (m MatchStarlarkExpr) Match(r *http.Request) bool {
234 input := string(m)
235 thread := new(starlark.Thread)
@@ -213,15 +243,16 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool {
243 return val.String() == "True"
244 }
245
216 -// matchRegexp is just the fields common among
217 -// matchers that can use regular expressions.
218 -type matchRegexp struct {
246 +// MatchRegexp is an embeddable type for matching
247 +// using regular expressions.
248 +type MatchRegexp struct {
249 Name string `json:"name"`
250 Pattern string `json:"pattern"`
251 compiled *regexp.Regexp
252 }
253
224 -func (mre *matchRegexp) Provision() error {
254 +// Provision compiles the regular expression.
255 +func (mre *MatchRegexp) Provision() error {
256 re, err := regexp.Compile(mre.Pattern)
257 if err != nil {
258 return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
@@ -230,14 +261,21 @@ func (mre *matchRegexp) Provision() error {
261 return nil
262 }
263
233 -func (mre *matchRegexp) Validate() error {
264 +// Validate ensures mre is set up correctly.
265 +func (mre *MatchRegexp) Validate() error {
266 if mre.Name != "" && !wordRE.MatchString(mre.Name) {
267 return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
268 }
269 return nil
270 }
271
240 -func (mre *matchRegexp) match(input string, repl caddy2.Replacer, scope string) bool {
272 +// Match returns true if input matches the compiled regular
273 +// expression in mre. It sets values on the replacer repl
274 +// associated with capture groups, using the given scope
275 +// (namespace). Capture groups stored to repl will take on
276 +// the name "http.matchers.<scope>.<mre.Name>.<N>" where
277 +// <N> is the name or number of the capture group.
278 +func (mre *MatchRegexp) Match(input string, repl caddy2.Replacer, scope string) bool {
279 matches := mre.compiled.FindStringSubmatch(input)
280 if matches == nil {
281 return false
modules/caddyhttp/matchers_test.go
+10 -10
@@ -176,38 +176,38 @@ func TestPathREMatcher(t *testing.T) {
176 expect: true,
177 },
178 {
179 - match: MatchPathRE{matchRegexp{Pattern: "/"}},
179 + match: MatchPathRE{MatchRegexp{Pattern: "/"}},
180 input: "/",
181 expect: true,
182 },
183 {
184 - match: MatchPathRE{matchRegexp{Pattern: "/foo"}},
184 + match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
185 input: "/foo",
186 expect: true,
187 },
188 {
189 - match: MatchPathRE{matchRegexp{Pattern: "/foo"}},
189 + match: MatchPathRE{MatchRegexp{Pattern: "/foo"}},
190 input: "/foo/",
191 expect: true,
192 },
193 {
194 - match: MatchPathRE{matchRegexp{Pattern: "/bar"}},
194 + match: MatchPathRE{MatchRegexp{Pattern: "/bar"}},
195 input: "/foo/",
196 expect: false,
197 },
198 {
199 - match: MatchPathRE{matchRegexp{Pattern: "^/bar"}},
199 + match: MatchPathRE{MatchRegexp{Pattern: "^/bar"}},
200 input: "/foo/bar",
201 expect: false,
202 },
203 {
204 - match: MatchPathRE{matchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
204 + match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
205 input: "/foo/bar/baz",
206 expect: true,
207 expectRepl: map[string]string{"name.1": "bar"},
208 },
209 {
210 - match: MatchPathRE{matchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
210 + match: MatchPathRE{MatchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
211 input: "/foo/bar/baz",
212 expect: true,
213 expectRepl: map[string]string{"name.myparam": "bar"},
@@ -315,17 +315,17 @@ func TestHeaderREMatcher(t *testing.T) {
315 expectRepl map[string]string
316 }{
317 {
318 - match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "foo"}},
318 + match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "foo"}},
319 input: http.Header{"Field": []string{"foo"}},
320 expect: true,
321 },
322 {
323 - match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "$foo^"}},
323 + match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "$foo^"}},
324 input: http.Header{"Field": []string{"foobar"}},
325 expect: false,
326 },
327 {
328 - match: MatchHeaderRE{"Field": &matchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
328 + match: MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
329 input: http.Header{"Field": []string{"foobar"}},
330 expect: true,
331 expectRepl: map[string]string{"name.1": "bar"},
modules/caddyhttp/routes.go
+45 -19
@@ -12,17 +12,42 @@ import (
12 // middlewares, and a responder for handling HTTP
13 // requests.
14 type ServerRoute struct {
15 - Group string `json:"group,omitempty"`
16 - Matchers map[string]json.RawMessage `json:"match,omitempty"`
17 - Apply []json.RawMessage `json:"apply,omitempty"`
18 - Respond json.RawMessage `json:"respond,omitempty"`
15 + Group string `json:"group,omitempty"`
16 + MatcherSets []map[string]json.RawMessage `json:"match,omitempty"`
17 + Apply []json.RawMessage `json:"apply,omitempty"`
18 + Respond json.RawMessage `json:"respond,omitempty"`
19
20 Terminal bool `json:"terminal,omitempty"`
21
22 // decoded values
23 - matchers []RequestMatcher
24 - middleware []MiddlewareHandler
25 - responder Handler
23 + matcherSets []MatcherSet
24 + middleware []MiddlewareHandler
25 + responder Handler
26 +}
27 +
28 +func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool {
29 + for _, ms := range sr.matcherSets {
30 + if ms.Match(r) {
31 + return true
32 + }
33 + }
34 + return false
35 +}
36 +
37 +// MatcherSet is a set of matchers which
38 +// must all match in order for the request
39 +// to be matched successfully.
40 +type MatcherSet []RequestMatcher
41 +
42 +// Match returns true if the request matches all
43 +// matchers in mset.
44 +func (mset MatcherSet) Match(r *http.Request) bool {
45 + for _, m := range mset {
46 + if !m.Match(r) {
47 + return false
48 + }
49 + }
50 + return true
51 }
52
53 // RouteList is a list of server routes that can
@@ -33,14 +58,18 @@ type RouteList []ServerRoute
58 func (routes RouteList) Provision(ctx caddy2.Context) error {
59 for i, route := range routes {
60 // matchers
36 - for modName, rawMsg := range route.Matchers {
37 - val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
38 - if err != nil {
39 - return fmt.Errorf("loading matcher module '%s': %v", modName, err)
61 + for _, matcherSet := range route.MatcherSets {
62 + var matchers MatcherSet
63 + for modName, rawMsg := range matcherSet {
64 + val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
65 + if err != nil {
66 + return fmt.Errorf("loading matcher module '%s': %v", modName, err)
67 + }
68 + matchers = append(matchers, val.(RequestMatcher))
69 }
41 - routes[i].matchers = append(routes[i].matchers, val.(RequestMatcher))
70 + routes[i].matcherSets = append(routes[i].matcherSets, matchers)
71 }
43 - routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
72 + routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help?
73
74 // middleware
75 for j, rawMsg := range route.Apply {
@@ -78,13 +107,10 @@ func (routes RouteList) BuildCompositeRoute(rw http.ResponseWriter, req *http.Re
107 var responder Handler
108 groups := make(map[string]struct{})
109
81 -routeLoop:
110 for _, route := range routes {
83 - // see if route matches
84 - for _, m := range route.matchers {
85 - if !m.Match(req) {
86 - continue routeLoop
87 - }
111 + // route must match at least one of the matcher sets
112 + if !route.anyMatcherSetMatches(req) {
113 + continue
114 }
115
116 // if route is part of a group, ensure only