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
caddyhttp: Implement better HTTP matchers including regexp; add tests
Matthew Holt committed
May 10, 2019 at 21:07 UTC
8ae0d6a509fd1b871457cf742369af04346933a8
5 files changed
+519
-66
modules/caddyhttp/matchers.go
+160
-39
@@ -1,8 +1,12 @@
1
package caddyhttp
2
3
import (
4
+ "fmt"
5
"log"
6
"net/http"
7
+ "net/textproto"
8
+ "net/url"
9
+ "regexp"
10
"strings"
11
12
"bitbucket.org/lightcodelabs/caddy2"
@@ -10,15 +14,16 @@ import (
14
"go.starlark.net/starlark"
15
)
16
13
-// TODO: Matchers should probably support regex of some sort... performance trade-offs?
17
type (
18
matchHost []string
19
matchPath []string
20
+ matchPathRE struct{ matchRegexp }
21
matchMethod []string
18
- matchQuery map[string][]string
19
- matchHeader map[string][]string
22
+ matchQuery url.Values
23
+ matchHeader http.Header
24
+ matchHeaderRE map[string]*matchRegexp
25
matchProtocol string
21
- matchScript string
26
+ matchStarlark string
27
)
28
29
func init() {
@@ -30,6 +35,10 @@ func init() {
35
Name: "http.matchers.path",
36
New: func() (interface{}, error) { return matchPath{}, nil },
37
})
38
+ caddy2.RegisterModule(caddy2.Module{
39
+ Name: "http.matchers.path_regexp",
40
+ New: func() (interface{}, error) { return new(matchPathRE), nil },
41
+ })
42
caddy2.RegisterModule(caddy2.Module{
43
Name: "http.matchers.method",
44
New: func() (interface{}, error) { return matchMethod{}, nil },
@@ -42,45 +51,39 @@ func init() {
51
Name: "http.matchers.header",
52
New: func() (interface{}, error) { return matchHeader{}, nil },
53
})
54
+ caddy2.RegisterModule(caddy2.Module{
55
+ Name: "http.matchers.header_regexp",
56
+ New: func() (interface{}, error) { return matchHeaderRE{}, nil },
57
+ })
58
caddy2.RegisterModule(caddy2.Module{
59
Name: "http.matchers.protocol",
60
New: func() (interface{}, error) { return new(matchProtocol), nil },
61
})
62
caddy2.RegisterModule(caddy2.Module{
63
Name: "http.matchers.caddyscript",
51
- New: func() (interface{}, error) { return new(matchScript), nil },
64
+ New: func() (interface{}, error) { return new(matchStarlark), nil },
65
})
66
}
67
55
-func (m matchScript) Match(r *http.Request) bool {
56
- input := string(m)
57
- thread := new(starlark.Thread)
58
- env := caddyscript.MatcherEnv(r)
59
- val, err := starlark.Eval(thread, "", input, env)
60
- if err != nil {
61
- log.Printf("caddyscript for matcher is invalid: attempting to evaluate expression `%v` error `%v`", input, err)
62
- return false
63
- }
64
-
65
- return val.String() == "True"
66
-}
67
-
68
-func (m matchProtocol) Match(r *http.Request) bool {
69
- switch string(m) {
70
- case "grpc":
71
- return r.Header.Get("content-type") == "application/grpc"
72
- case "https":
73
- return r.TLS != nil
74
- case "http":
75
- return r.TLS == nil
76
- }
77
-
78
- return false
79
-}
80
-
68
func (m matchHost) Match(r *http.Request) bool {
69
+outer:
70
for _, host := range m {
83
- if r.Host == host {
71
+ if strings.Contains(host, "*") {
72
+ patternParts := strings.Split(host, ".")
73
+ incomingParts := strings.Split(r.Host, ".")
74
+ if len(patternParts) != len(incomingParts) {
75
+ continue
76
+ }
77
+ for i := range patternParts {
78
+ if patternParts[i] == "*" {
79
+ continue
80
+ }
81
+ if !strings.EqualFold(patternParts[i], incomingParts[i]) {
82
+ continue outer
83
+ }
84
+ }
85
+ return true
86
+ } else if strings.EqualFold(r.Host, host) {
87
return true
88
}
89
}
@@ -96,6 +99,11 @@ func (m matchPath) Match(r *http.Request) bool {
99
return false
100
}
101
102
+func (m matchPathRE) Match(r *http.Request) bool {
103
+ repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
104
+ return m.match(r.URL.Path, repl, "path_regexp")
105
+}
106
+
107
func (m matchMethod) Match(r *http.Request) bool {
108
for _, method := range m {
109
if r.Method == method {
@@ -118,26 +126,139 @@ func (m matchQuery) Match(r *http.Request) bool {
126
}
127
128
func (m matchHeader) Match(r *http.Request) bool {
121
- for field, vals := range m {
122
- fieldVals := r.Header[field]
123
- for _, fieldVal := range fieldVals {
124
- for _, v := range vals {
125
- if fieldVal == v {
126
- return true
129
+ for field, allowedFieldVals := range m {
130
+ var match bool
131
+ actualFieldVals := r.Header[textproto.CanonicalMIMEHeaderKey(field)]
132
+ fieldVals:
133
+ for _, actualFieldVal := range actualFieldVals {
134
+ for _, allowedFieldVal := range allowedFieldVals {
135
+ if actualFieldVal == allowedFieldVal {
136
+ match = true
137
+ break fieldVals
138
}
139
}
140
}
141
+ if !match {
142
+ return false
143
+ }
144
+ }
145
+ return true
146
+}
147
+
148
+func (m matchHeaderRE) Match(r *http.Request) bool {
149
+ for field, rm := range m {
150
+ repl := r.Context().Value(ReplacerCtxKey).(*Replacer)
151
+ match := rm.match(r.Header.Get(field), repl, "header_regexp")
152
+ if !match {
153
+ return false
154
+ }
155
+ }
156
+ return true
157
+}
158
+
159
+func (m matchHeaderRE) Provision() error {
160
+ for _, rm := range m {
161
+ err := rm.Provision()
162
+ if err != nil {
163
+ return err
164
+ }
165
+ }
166
+ return nil
167
+}
168
+
169
+func (m matchHeaderRE) Validate() error {
170
+ for _, rm := range m {
171
+ err := rm.Validate()
172
+ if err != nil {
173
+ return err
174
+ }
175
+ }
176
+ return nil
177
+}
178
+
179
+func (m matchProtocol) Match(r *http.Request) bool {
180
+ switch string(m) {
181
+ case "grpc":
182
+ return r.Header.Get("content-type") == "application/grpc"
183
+ case "https":
184
+ return r.TLS != nil
185
+ case "http":
186
+ return r.TLS == nil
187
}
188
return false
189
}
190
191
+func (m matchStarlark) Match(r *http.Request) bool {
192
+ input := string(m)
193
+ thread := new(starlark.Thread)
194
+ env := caddyscript.MatcherEnv(r)
195
+ val, err := starlark.Eval(thread, "", input, env)
196
+ if err != nil {
197
+ // TODO: Can we detect this in Provision or Validate instead?
198
+ log.Printf("caddyscript for matcher is invalid: attempting to evaluate expression `%v` error `%v`", input, err)
199
+ return false
200
+ }
201
+ return val.String() == "True"
202
+}
203
+
204
+// matchRegexp is just the fields common among
205
+// matchers that can use regular expressions.
206
+type matchRegexp struct {
207
+ Name string `json:"name"`
208
+ Pattern string `json:"pattern"`
209
+ compiled *regexp.Regexp
210
+}
211
+
212
+func (mre *matchRegexp) Provision() error {
213
+ re, err := regexp.Compile(mre.Pattern)
214
+ if err != nil {
215
+ return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
216
+ }
217
+ mre.compiled = re
218
+ return nil
219
+}
220
+
221
+func (mre *matchRegexp) Validate() error {
222
+ if mre.Name != "" && !wordRE.MatchString(mre.Name) {
223
+ return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
224
+ }
225
+ return nil
226
+}
227
+
228
+func (mre *matchRegexp) match(input string, repl *Replacer, scope string) bool {
229
+ matches := mre.compiled.FindStringSubmatch(input)
230
+ if matches == nil {
231
+ return false
232
+ }
233
+
234
+ // save all capture groups, first by index
235
+ for i, match := range matches {
236
+ key := fmt.Sprintf("http.matchers.%s.%s.%d", scope, mre.Name, i)
237
+ repl.Map(key, match)
238
+ }
239
+
240
+ // then by name
241
+ for i, name := range mre.compiled.SubexpNames() {
242
+ if i != 0 && name != "" {
243
+ key := fmt.Sprintf("http.matchers.%s.%s.%s", scope, mre.Name, name)
244
+ repl.Map(key, matches[i])
245
+ }
246
+ }
247
+
248
+ return true
249
+}
250
+
251
+var wordRE = regexp.MustCompile(`\w+`)
252
+
253
// Interface guards
254
var (
255
_ RouteMatcher = (*matchHost)(nil)
256
_ RouteMatcher = (*matchPath)(nil)
257
+ _ RouteMatcher = (*matchPathRE)(nil)
258
_ RouteMatcher = (*matchMethod)(nil)
259
_ RouteMatcher = (*matchQuery)(nil)
260
_ RouteMatcher = (*matchHeader)(nil)
261
+ _ RouteMatcher = (*matchHeaderRE)(nil)
262
_ RouteMatcher = (*matchProtocol)(nil)
142
- _ RouteMatcher = (*matchScript)(nil)
263
+ _ RouteMatcher = (*matchStarlark)(nil)
264
)
modules/caddyhttp/matchers_test.go
new
+346
@@ -0,0 +1,346 @@
1
+package caddyhttp
2
+
3
+import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "net/url"
9
+ "testing"
10
+)
11
+
12
+func TestHostMatcher(t *testing.T) {
13
+ for i, tc := range []struct {
14
+ match matchHost
15
+ input string
16
+ expect bool
17
+ }{
18
+ {
19
+ match: matchHost{},
20
+ input: "example.com",
21
+ expect: false,
22
+ },
23
+ {
24
+ match: matchHost{"example.com"},
25
+ input: "example.com",
26
+ expect: true,
27
+ },
28
+ {
29
+ match: matchHost{"example.com"},
30
+ input: "foo.example.com",
31
+ expect: false,
32
+ },
33
+ {
34
+ match: matchHost{"example.com"},
35
+ input: "EXAMPLE.COM",
36
+ expect: true,
37
+ },
38
+ {
39
+ match: matchHost{"foo.example.com"},
40
+ input: "foo.example.com",
41
+ expect: true,
42
+ },
43
+ {
44
+ match: matchHost{"foo.example.com"},
45
+ input: "bar.example.com",
46
+ expect: false,
47
+ },
48
+ {
49
+ match: matchHost{"*.example.com"},
50
+ input: "example.com",
51
+ expect: false,
52
+ },
53
+ {
54
+ match: matchHost{"*.example.com"},
55
+ input: "foo.example.com",
56
+ expect: true,
57
+ },
58
+ {
59
+ match: matchHost{"*.example.com"},
60
+ input: "foo.bar.example.com",
61
+ expect: false,
62
+ },
63
+ {
64
+ match: matchHost{"*.example.com", "example.net"},
65
+ input: "example.net",
66
+ expect: true,
67
+ },
68
+ {
69
+ match: matchHost{"example.net", "*.example.com"},
70
+ input: "foo.example.com",
71
+ expect: true,
72
+ },
73
+ {
74
+ match: matchHost{"*.example.net", "*.*.example.com"},
75
+ input: "foo.bar.example.com",
76
+ expect: true,
77
+ },
78
+ {
79
+ match: matchHost{"*.example.net", "sub.*.example.com"},
80
+ input: "sub.foo.example.com",
81
+ expect: true,
82
+ },
83
+ {
84
+ match: matchHost{"*.example.net", "sub.*.example.com"},
85
+ input: "sub.foo.example.net",
86
+ expect: false,
87
+ },
88
+ } {
89
+ req := &http.Request{Host: tc.input}
90
+ actual := tc.match.Match(req)
91
+ if actual != tc.expect {
92
+ t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
93
+ continue
94
+ }
95
+ }
96
+}
97
+
98
+func TestPathMatcher(t *testing.T) {
99
+ for i, tc := range []struct {
100
+ match matchPath
101
+ input string
102
+ expect bool
103
+ }{
104
+ {
105
+ match: matchPath{},
106
+ input: "/",
107
+ expect: false,
108
+ },
109
+ {
110
+ match: matchPath{"/"},
111
+ input: "/",
112
+ expect: true,
113
+ },
114
+ {
115
+ match: matchPath{"/foo/bar"},
116
+ input: "/",
117
+ expect: false,
118
+ },
119
+ {
120
+ match: matchPath{"/foo/bar"},
121
+ input: "/foo/bar",
122
+ expect: true,
123
+ },
124
+ {
125
+ match: matchPath{"/foo/bar/"},
126
+ input: "/foo/bar",
127
+ expect: false,
128
+ },
129
+ {
130
+ match: matchPath{"/foo/bar/", "/other"},
131
+ input: "/other/",
132
+ expect: true,
133
+ },
134
+ } {
135
+ req := &http.Request{URL: &url.URL{Path: tc.input}}
136
+ actual := tc.match.Match(req)
137
+ if actual != tc.expect {
138
+ t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
139
+ continue
140
+ }
141
+ }
142
+}
143
+
144
+func TestPathREMatcher(t *testing.T) {
145
+ for i, tc := range []struct {
146
+ match matchPathRE
147
+ input string
148
+ expect bool
149
+ expectRepl map[string]string
150
+ }{
151
+ {
152
+ match: matchPathRE{},
153
+ input: "/",
154
+ expect: true,
155
+ },
156
+ {
157
+ match: matchPathRE{matchRegexp{Pattern: "/"}},
158
+ input: "/",
159
+ expect: true,
160
+ },
161
+ {
162
+ match: matchPathRE{matchRegexp{Pattern: "/foo"}},
163
+ input: "/foo",
164
+ expect: true,
165
+ },
166
+ {
167
+ match: matchPathRE{matchRegexp{Pattern: "/foo"}},
168
+ input: "/foo/",
169
+ expect: true,
170
+ },
171
+ {
172
+ match: matchPathRE{matchRegexp{Pattern: "/bar"}},
173
+ input: "/foo/",
174
+ expect: false,
175
+ },
176
+ {
177
+ match: matchPathRE{matchRegexp{Pattern: "^/bar"}},
178
+ input: "/foo/bar",
179
+ expect: false,
180
+ },
181
+ {
182
+ match: matchPathRE{matchRegexp{Pattern: "^/foo/(.*)/baz$", Name: "name"}},
183
+ input: "/foo/bar/baz",
184
+ expect: true,
185
+ expectRepl: map[string]string{"name.1": "bar"},
186
+ },
187
+ {
188
+ match: matchPathRE{matchRegexp{Pattern: "^/foo/(?P<myparam>.*)/baz$", Name: "name"}},
189
+ input: "/foo/bar/baz",
190
+ expect: true,
191
+ expectRepl: map[string]string{"name.myparam": "bar"},
192
+ },
193
+ } {
194
+ // compile the regexp and validate its name
195
+ err := tc.match.Provision()
196
+ if err != nil {
197
+ t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
198
+ continue
199
+ }
200
+ err = tc.match.Validate()
201
+ if err != nil {
202
+ t.Errorf("Test %d %v: Validating: %v", i, tc.match, err)
203
+ continue
204
+ }
205
+
206
+ // set up the fake request and its Replacer
207
+ req := &http.Request{URL: &url.URL{Path: tc.input}}
208
+ repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)}
209
+ ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
210
+ req = req.WithContext(ctx)
211
+
212
+ actual := tc.match.Match(req)
213
+ if actual != tc.expect {
214
+ t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
215
+ i, tc.match.Pattern, tc.expect, actual, tc.input)
216
+ continue
217
+ }
218
+
219
+ for key, expectVal := range tc.expectRepl {
220
+ placeholder := fmt.Sprintf("{http.matchers.path_regexp.%s}", key)
221
+ actualVal := repl.Replace(placeholder, "<empty>")
222
+ if actualVal != expectVal {
223
+ t.Errorf("Test %d [%v]: Expected placeholder {http.matchers.path_regexp.%s} to be '%s' but got '%s'",
224
+ i, tc.match.Pattern, key, expectVal, actualVal)
225
+ continue
226
+ }
227
+ }
228
+ }
229
+}
230
+
231
+func TestHeaderMatcher(t *testing.T) {
232
+ for i, tc := range []struct {
233
+ match matchHeader
234
+ input http.Header // make sure these are canonical cased (std lib will do that in a real request)
235
+ expect bool
236
+ }{
237
+ {
238
+ match: matchHeader{"Field": []string{"foo"}},
239
+ input: http.Header{"Field": []string{"foo"}},
240
+ expect: true,
241
+ },
242
+ {
243
+ match: matchHeader{"Field": []string{"foo", "bar"}},
244
+ input: http.Header{"Field": []string{"bar"}},
245
+ expect: true,
246
+ },
247
+ {
248
+ match: matchHeader{"Field": []string{"foo", "bar"}},
249
+ input: http.Header{"Alakazam": []string{"kapow"}},
250
+ expect: false,
251
+ },
252
+ {
253
+ match: matchHeader{"Field": []string{"foo", "bar"}},
254
+ input: http.Header{"Field": []string{"kapow"}},
255
+ expect: false,
256
+ },
257
+ {
258
+ match: matchHeader{"Field": []string{"foo", "bar"}},
259
+ input: http.Header{"Field": []string{"kapow", "foo"}},
260
+ expect: true,
261
+ },
262
+ {
263
+ match: matchHeader{"Field1": []string{"foo"}, "Field2": []string{"bar"}},
264
+ input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}},
265
+ expect: true,
266
+ },
267
+ {
268
+ match: matchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}},
269
+ input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"bar"}},
270
+ expect: true,
271
+ },
272
+ {
273
+ match: matchHeader{"field1": []string{"foo"}, "field2": []string{"bar"}},
274
+ input: http.Header{"Field1": []string{"foo"}, "Field2": []string{"kapow"}},
275
+ expect: false,
276
+ },
277
+ } {
278
+ req := &http.Request{Header: tc.input}
279
+ actual := tc.match.Match(req)
280
+ if actual != tc.expect {
281
+ t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
282
+ continue
283
+ }
284
+ }
285
+}
286
+
287
+func TestHeaderREMatcher(t *testing.T) {
288
+ for i, tc := range []struct {
289
+ match matchHeaderRE
290
+ input http.Header // make sure these are canonical cased (std lib will do that in a real request)
291
+ expect bool
292
+ expectRepl map[string]string
293
+ }{
294
+ {
295
+ match: matchHeaderRE{"Field": &matchRegexp{Pattern: "foo"}},
296
+ input: http.Header{"Field": []string{"foo"}},
297
+ expect: true,
298
+ },
299
+ {
300
+ match: matchHeaderRE{"Field": &matchRegexp{Pattern: "$foo^"}},
301
+ input: http.Header{"Field": []string{"foobar"}},
302
+ expect: false,
303
+ },
304
+ {
305
+ match: matchHeaderRE{"Field": &matchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
306
+ input: http.Header{"Field": []string{"foobar"}},
307
+ expect: true,
308
+ expectRepl: map[string]string{"name.1": "bar"},
309
+ },
310
+ } {
311
+ // compile the regexp and validate its name
312
+ err := tc.match.Provision()
313
+ if err != nil {
314
+ t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
315
+ continue
316
+ }
317
+ err = tc.match.Validate()
318
+ if err != nil {
319
+ t.Errorf("Test %d %v: Validating: %v", i, tc.match, err)
320
+ continue
321
+ }
322
+
323
+ // set up the fake request and its Replacer
324
+ req := &http.Request{Header: tc.input, URL: new(url.URL)}
325
+ repl := &Replacer{req: req, resp: httptest.NewRecorder(), custom: make(map[string]string)}
326
+ ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
327
+ req = req.WithContext(ctx)
328
+
329
+ actual := tc.match.Match(req)
330
+ if actual != tc.expect {
331
+ t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
332
+ i, tc.match, tc.expect, actual, tc.input)
333
+ continue
334
+ }
335
+
336
+ for key, expectVal := range tc.expectRepl {
337
+ placeholder := fmt.Sprintf("{http.matchers.header_regexp.%s}", key)
338
+ actualVal := repl.Replace(placeholder, "<empty>")
339
+ if actualVal != expectVal {
340
+ t.Errorf("Test %d [%v]: Expected placeholder {http.matchers.header_regexp.%s} to be '%s' but got '%s'",
341
+ i, tc.match, key, expectVal, actualVal)
342
+ continue
343
+ }
344
+ }
345
+ }
346
+}
modules/caddyhttp/replacer.go
+5
-4
@@ -8,7 +8,9 @@ import (
8
)
9
10
// Replacer can replace values in strings based
11
-// on a request and/or response writer.
11
+// on a request and/or response writer. The zero
12
+// Replacer is not valid; it must be initialized
13
+// within this package.
14
type Replacer struct {
15
req *http.Request
16
resp http.ResponseWriter
@@ -75,18 +77,17 @@ func (r *Replacer) defaults() map[string]string {
77
}(),
78
}
79
80
+ // TODO: why should header fields, cookies, and query params get special treatment like this?
81
+ // maybe they should be scoped by words like "request.header." just like everything else.
82
for field, vals := range r.req.Header {
83
m[">"+strings.ToLower(field)] = strings.Join(vals, ",")
84
}
81
-
85
for field, vals := range r.resp.Header() {
86
m["<"+strings.ToLower(field)] = strings.Join(vals, ",")
87
}
85
-
88
for _, cookie := range r.req.Cookies() {
89
m["~"+cookie.Name] = cookie.Value
90
}
89
-
91
for param, vals := range r.req.URL.Query() {
92
m["?"+param] = strings.Join(vals, ",")
93
}
modules/caddyhttp/responsewriter.go
+6
-19
@@ -7,16 +7,13 @@ import (
7
"net/http"
8
)
9
10
-// TODO: Is this type really required? Wouldn't embedding the
11
-// default ResponseWriter always work too, when wrapping it?
12
-
10
// ResponseWriterWrapper wraps an underlying ResponseWriter and
14
-// promotes its Pusher/Flusher/CloseNotifier/Hijacker methods
15
-// as well. To use this type, embed a pointer to it within your
16
-// own struct type that implements the http.ResponseWriter
17
-// interface, then call methods on the embedded value. You can
18
-// make sure your type wraps correctly by asserting that it
19
-// implements the HTTPInterfaces interface.
11
+// promotes its Pusher/Flusher/Hijacker methods as well. To use
12
+// this type, embed a pointer to it within your own struct type
13
+// that implements the http.ResponseWriter interface, then call
14
+// methods on the embedded value. You can make sure your type
15
+// wraps correctly by asserting that it implements the
16
+// HTTPInterfaces interface.
17
type ResponseWriterWrapper struct {
18
http.ResponseWriter
19
}
@@ -40,15 +37,6 @@ func (rww *ResponseWriterWrapper) Flush() {
37
}
38
}
39
43
-// CloseNotify implements http.CloseNotifier. It simply calls the underlying
44
-// ResponseWriter's CloseNotify method if there is one, or panics.
45
-func (rww *ResponseWriterWrapper) CloseNotify() <-chan bool {
46
- if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
47
- return cn.CloseNotify()
48
- }
49
- panic("not a close notifier")
50
-}
51
-
40
// Push implements http.Pusher. It simply calls the underlying
41
// ResponseWriter's Push method if there is one, or returns an error.
42
func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) error {
@@ -63,7 +51,6 @@ type HTTPInterfaces interface {
51
http.ResponseWriter
52
http.Pusher
53
http.Flusher
66
- http.CloseNotifier
54
http.Hijacker
55
}
56
modules/caddyhttp/routes.go
+2
-4
@@ -13,7 +13,7 @@ type serverRoute struct {
13
Apply []json.RawMessage `json:"apply"`
14
Respond json.RawMessage `json:"respond"`
15
16
- Exclusive bool `json:"exclusive"`
16
+ Terminal bool `json:"terminal"`
17
18
// decoded values
19
matchers []RouteMatcher
@@ -49,9 +49,7 @@ routeLoop:
49
if responder == nil {
50
responder = route.responder
51
}
52
- // TODO: Should exclusive apply to only middlewares, or responder too?
53
- // i.e. what if they haven't set a responder yet, but the first middleware chain is exclusive...
54
- if route.Exclusive {
52
+ if route.Terminal {
53
break
54
}
55
}