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