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
httpcaddyfile: New `handle_path` directive (#3281)
* caddyconfig: WIP implementation of handle_path * caddyconfig: Complete the implementation - h.NewRoute was key * caddyconfig: Add handle_path integration test * caddyhttp: Use the path matcher as-is, strip the trailing *, update test
Francis Lavoie committed
May 26, 2020 at 17:27 UTC
8c5d00b2bc815c182e1a510be6dddc128949bf23
4 files changed
+186
-59
caddyconfig/httpcaddyfile/builtins.go
+2
-2
@@ -442,11 +442,11 @@ func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
442
}
443
444
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
445
- return parseSegmentAsSubroute(h)
445
+ return ParseSegmentAsSubroute(h)
446
}
447
448
func parseHandleErrors(h Helper) ([]ConfigValue, error) {
449
- subroute, err := parseSegmentAsSubroute(h)
449
+ subroute, err := ParseSegmentAsSubroute(h)
450
if err != nil {
451
return nil, err
452
}
caddyconfig/httpcaddyfile/directives.go
+58
-57
@@ -57,6 +57,7 @@ var directiveOrder = []string{
57
// special routing directives
58
"handle",
59
"route",
60
+ "handle_path",
61
62
// handlers that typically respond to requests
63
"respond",
@@ -261,6 +262,63 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
262
return []ConfigValue{{Class: "bind", Value: addrs}}
263
}
264
265
+// ParseSegmentAsSubroute parses the segment such that its subdirectives
266
+// are themselves treated as directives, from which a subroute is built
267
+// and returned.
268
+func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
269
+ var allResults []ConfigValue
270
+
271
+ for h.Next() {
272
+ // slice the linear list of tokens into top-level segments
273
+ var segments []caddyfile.Segment
274
+ for nesting := h.Nesting(); h.NextBlock(nesting); {
275
+ segments = append(segments, h.NextSegment())
276
+ }
277
+
278
+ // copy existing matcher definitions so we can augment
279
+ // new ones that are defined only in this scope
280
+ matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
281
+ for key, val := range h.matcherDefs {
282
+ matcherDefs[key] = val
283
+ }
284
+
285
+ // find and extract any embedded matcher definitions in this scope
286
+ for i, seg := range segments {
287
+ if strings.HasPrefix(seg.Directive(), matcherPrefix) {
288
+ err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
289
+ if err != nil {
290
+ return nil, err
291
+ }
292
+ segments = append(segments[:i], segments[i+1:]...)
293
+ }
294
+ }
295
+
296
+ // with matchers ready to go, evaluate each directive's segment
297
+ for _, seg := range segments {
298
+ dir := seg.Directive()
299
+ dirFunc, ok := registeredDirectives[dir]
300
+ if !ok {
301
+ return nil, h.Errf("unrecognized directive: %s", dir)
302
+ }
303
+
304
+ subHelper := h
305
+ subHelper.Dispenser = caddyfile.NewDispenser(seg)
306
+ subHelper.matcherDefs = matcherDefs
307
+
308
+ results, err := dirFunc(subHelper)
309
+ if err != nil {
310
+ return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
311
+ }
312
+ for _, result := range results {
313
+ result.directive = dir
314
+ allResults = append(allResults, result)
315
+ }
316
+ }
317
+ }
318
+
319
+ return buildSubroute(allResults, h.groupCounter)
320
+}
321
+
322
// ConfigValue represents a value to be added to the final
323
// configuration, or a value to be consulted when building
324
// the final configuration.
@@ -329,63 +387,6 @@ func sortRoutes(routes []ConfigValue) {
387
})
388
}
389
332
-// parseSegmentAsSubroute parses the segment such that its subdirectives
333
-// are themselves treated as directives, from which a subroute is built
334
-// and returned.
335
-func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
336
- var allResults []ConfigValue
337
-
338
- for h.Next() {
339
- // slice the linear list of tokens into top-level segments
340
- var segments []caddyfile.Segment
341
- for nesting := h.Nesting(); h.NextBlock(nesting); {
342
- segments = append(segments, h.NextSegment())
343
- }
344
-
345
- // copy existing matcher definitions so we can augment
346
- // new ones that are defined only in this scope
347
- matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
348
- for key, val := range h.matcherDefs {
349
- matcherDefs[key] = val
350
- }
351
-
352
- // find and extract any embedded matcher definitions in this scope
353
- for i, seg := range segments {
354
- if strings.HasPrefix(seg.Directive(), matcherPrefix) {
355
- err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
356
- if err != nil {
357
- return nil, err
358
- }
359
- segments = append(segments[:i], segments[i+1:]...)
360
- }
361
- }
362
-
363
- // with matchers ready to go, evaluate each directive's segment
364
- for _, seg := range segments {
365
- dir := seg.Directive()
366
- dirFunc, ok := registeredDirectives[dir]
367
- if !ok {
368
- return nil, h.Errf("unrecognized directive: %s", dir)
369
- }
370
-
371
- subHelper := h
372
- subHelper.Dispenser = caddyfile.NewDispenser(seg)
373
- subHelper.matcherDefs = matcherDefs
374
-
375
- results, err := dirFunc(subHelper)
376
- if err != nil {
377
- return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err)
378
- }
379
- for _, result := range results {
380
- result.directive = dir
381
- allResults = append(allResults, result)
382
- }
383
- }
384
- }
385
-
386
- return buildSubroute(allResults, h.groupCounter)
387
-}
388
-
390
// serverBlock pairs a Caddyfile server block with
391
// a "pile" of config values, keyed by class name,
392
// as well as its parsed keys for convenience.
caddytest/integration/caddyfile_adapt/handle_path.txt
new
+52
@@ -0,0 +1,52 @@
1
+:80
2
+handle_path /api/v1/* {
3
+ respond "API v1"
4
+}
5
+----------
6
+{
7
+ "apps": {
8
+ "http": {
9
+ "servers": {
10
+ "srv0": {
11
+ "listen": [
12
+ ":80"
13
+ ],
14
+ "routes": [
15
+ {
16
+ "match": [
17
+ {
18
+ "path": [
19
+ "/api/v1/*"
20
+ ]
21
+ }
22
+ ],
23
+ "handle": [
24
+ {
25
+ "handler": "subroute",
26
+ "routes": [
27
+ {
28
+ "handle": [
29
+ {
30
+ "handler": "rewrite",
31
+ "strip_path_prefix": "/api/v1"
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "handle": [
37
+ {
38
+ "body": "API v1",
39
+ "handler": "static_response"
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ }
50
+ }
51
+ }
52
+}
\ No newline at end of file
modules/caddyhttp/rewrite/caddyfile.go
+74
@@ -15,9 +15,12 @@
15
package rewrite
16
17
import (
18
+ "encoding/json"
19
"strconv"
20
"strings"
21
22
+ "github.com/caddyserver/caddy/v2"
23
+ "github.com/caddyserver/caddy/v2/caddyconfig"
24
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
25
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
26
)
@@ -25,6 +28,7 @@ import (
28
func init() {
29
httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfileRewrite)
30
httpcaddyfile.RegisterHandlerDirective("uri", parseCaddyfileURI)
31
+ httpcaddyfile.RegisterDirective("handle_path", parseCaddyfileHandlePath)
32
}
33
34
// parseCaddyfileRewrite sets up a basic rewrite handler from Caddyfile tokens. Syntax:
@@ -110,3 +114,73 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
114
}
115
return rewr, nil
116
}
117
+
118
+// parseCaddyfileHandlePath parses the handle_path directive. Syntax:
119
+//
120
+// handle_path [<matcher>] {
121
+// <directives...>
122
+// }
123
+//
124
+// Only path matchers (with a `/` prefix) are supported as this is a shortcut
125
+// for the handle directive with a strip_prefix rewrite.
126
+func parseCaddyfileHandlePath(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
127
+ if !h.Next() {
128
+ return nil, h.ArgErr()
129
+ }
130
+ if !h.NextArg() {
131
+ return nil, h.ArgErr()
132
+ }
133
+
134
+ // read the prefix to strip
135
+ path := h.Val()
136
+ if !strings.HasPrefix(path, "/") {
137
+ return nil, h.Errf("path matcher must begin with '/', got %s", path)
138
+ }
139
+
140
+ // we only want to strip what comes before the '/' if
141
+ // the user specified it (e.g. /api/* should only strip /api)
142
+ var stripPath string
143
+ if strings.HasSuffix(path, "/*") {
144
+ stripPath = path[:len(path)-2]
145
+ } else if strings.HasSuffix(path, "*") {
146
+ stripPath = path[:len(path)-1]
147
+ } else {
148
+ stripPath = path
149
+ }
150
+
151
+ // the ParseSegmentAsSubroute function expects the cursor
152
+ // to be at the token just before the block opening,
153
+ // so we need to rewind because we already read past it
154
+ h.Reset()
155
+ h.Next()
156
+
157
+ // parse the block contents as a subroute handler
158
+ handler, err := httpcaddyfile.ParseSegmentAsSubroute(h)
159
+ if err != nil {
160
+ return nil, err
161
+ }
162
+ subroute, ok := handler.(*caddyhttp.Subroute)
163
+ if !ok {
164
+ return nil, h.Errf("segment was not parsed as a subroute")
165
+ }
166
+
167
+ // make a matcher on the path and everything below it
168
+ pathMatcher := caddy.ModuleMap{
169
+ "path": h.JSON(caddyhttp.MatchPath{path}),
170
+ }
171
+
172
+ // build a route with a rewrite handler to strip the path prefix
173
+ route := caddyhttp.Route{
174
+ HandlersRaw: []json.RawMessage{
175
+ caddyconfig.JSONModuleObject(Rewrite{
176
+ StripPathPrefix: stripPath,
177
+ }, "handler", "rewrite", nil),
178
+ },
179
+ }
180
+
181
+ // prepend the route to the subroute
182
+ subroute.Routes = append([]caddyhttp.Route{route}, subroute.Routes...)
183
+
184
+ // build and return a route from the subroute
185
+ return h.NewRoute(pathMatcher, subroute), nil
186
+}