Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 01:45 UTC
caddyhttp: Implement named routes, `invoke` directive (#5107)
* caddyhttp: Implement named routes, `invoke` directive * gofmt * Add experimental marker * Adjust route compile comments
Francis Lavoie committed
May 16, 2023 at 11:27 UTC
cbf16f6d9eb77f37d6eb588ff3e54cfdfddecc21
9 files changed
+464
-29
caddyconfig/caddyfile/parse.go
+42
-8
@@ -148,7 +148,6 @@ func (p *parser) begin() error {
148
}
149
150
err := p.addresses()
151
-
151
if err != nil {
152
return err
153
}
@@ -159,6 +158,25 @@ func (p *parser) begin() error {
158
return nil
159
}
160
161
+ if ok, name := p.isNamedRoute(); ok {
162
+ // named routes only have one key, the route name
163
+ p.block.Keys = []string{name}
164
+ p.block.IsNamedRoute = true
165
+
166
+ // we just need a dummy leading token to ease parsing later
167
+ nameToken := p.Token()
168
+ nameToken.Text = name
169
+
170
+ // get all the tokens from the block, including the braces
171
+ tokens, err := p.blockTokens(true)
172
+ if err != nil {
173
+ return err
174
+ }
175
+ tokens = append([]Token{nameToken}, tokens...)
176
+ p.block.Segments = []Segment{tokens}
177
+ return nil
178
+ }
179
+
180
if ok, name := p.isSnippet(); ok {
181
if p.definedSnippets == nil {
182
p.definedSnippets = map[string][]Token{}
@@ -167,7 +185,7 @@ func (p *parser) begin() error {
185
return p.Errf("redeclaration of previously declared snippet %s", name)
186
}
187
// consume all tokens til matched close brace
170
- tokens, err := p.snippetTokens()
188
+ tokens, err := p.blockTokens(false)
189
if err != nil {
190
return err
191
}
@@ -576,6 +594,15 @@ func (p *parser) closeCurlyBrace() error {
594
return nil
595
}
596
597
+func (p *parser) isNamedRoute() (bool, string) {
598
+ keys := p.block.Keys
599
+ // A named route block is a single key with parens, prefixed with &.
600
+ if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") {
601
+ return true, strings.TrimSuffix(keys[0][2:], ")")
602
+ }
603
+ return false, ""
604
+}
605
+
606
func (p *parser) isSnippet() (bool, string) {
607
keys := p.block.Keys
608
// A snippet block is a single key with parens. Nothing else qualifies.
@@ -586,18 +613,24 @@ func (p *parser) isSnippet() (bool, string) {
613
}
614
615
// read and store everything in a block for later replay.
589
-func (p *parser) snippetTokens() ([]Token, error) {
590
- // snippet must have curlies.
616
+func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
617
+ // block must have curlies.
618
err := p.openCurlyBrace()
619
if err != nil {
620
return nil, err
621
}
595
- nesting := 1 // count our own nesting in snippets
622
+ nesting := 1 // count our own nesting
623
tokens := []Token{}
624
+ if retainCurlies {
625
+ tokens = append(tokens, p.Token())
626
+ }
627
for p.Next() {
628
if p.Val() == "}" {
629
nesting--
630
if nesting == 0 {
631
+ if retainCurlies {
632
+ tokens = append(tokens, p.Token())
633
+ }
634
break
635
}
636
}
@@ -617,9 +650,10 @@ func (p *parser) snippetTokens() ([]Token, error) {
650
// head of the server block with tokens, which are
651
// grouped by segments.
652
type ServerBlock struct {
620
- HasBraces bool
621
- Keys []string
622
- Segments []Segment
653
+ HasBraces bool
654
+ Keys []string
655
+ Segments []Segment
656
+ IsNamedRoute bool
657
}
658
659
// DispenseDirective returns a dispenser that contains
caddyconfig/httpcaddyfile/builtins.go
+22
@@ -48,6 +48,7 @@ func init() {
48
RegisterHandlerDirective("route", parseRoute)
49
RegisterHandlerDirective("handle", parseHandle)
50
RegisterDirective("handle_errors", parseHandleErrors)
51
+ RegisterHandlerDirective("invoke", parseInvoke)
52
RegisterDirective("log", parseLog)
53
RegisterHandlerDirective("skip_log", parseSkipLog)
54
}
@@ -764,6 +765,27 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
765
}, nil
766
}
767
768
+// parseInvoke parses the invoke directive.
769
+func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
770
+ h.Next() // consume directive
771
+ if !h.NextArg() {
772
+ return nil, h.ArgErr()
773
+ }
774
+ for h.Next() || h.NextBlock(0) {
775
+ return nil, h.ArgErr()
776
+ }
777
+
778
+ // remember that we're invoking this name
779
+ // to populate the server with these named routes
780
+ if h.State[namedRouteKey] == nil {
781
+ h.State[namedRouteKey] = map[string]struct{}{}
782
+ }
783
+ h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{}
784
+
785
+ // return the handler
786
+ return &caddyhttp.Invoke{Name: h.Val()}, nil
787
+}
788
+
789
// parseLog parses the log directive. Syntax:
790
//
791
// log {
caddyconfig/httpcaddyfile/directives.go
+1
@@ -65,6 +65,7 @@ var directiveOrder = []string{
65
"templates",
66
67
// special routing & dispatching directives
68
+ "invoke",
69
"handle",
70
"handle_path",
71
"route",
caddyconfig/httpcaddyfile/httptype.go
+111
-2
@@ -52,8 +52,10 @@ type ServerType struct {
52
}
53
54
// Setup makes a config from the tokens.
55
-func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
56
- options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
55
+func (st ServerType) Setup(
56
+ inputServerBlocks []caddyfile.ServerBlock,
57
+ options map[string]any,
58
+) (*caddy.Config, []caddyconfig.Warning, error) {
59
var warnings []caddyconfig.Warning
60
gc := counter{new(int)}
61
state := make(map[string]any)
@@ -79,6 +81,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
81
return nil, warnings, err
82
}
83
84
+ originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings)
85
+ if err != nil {
86
+ return nil, warnings, err
87
+ }
88
+
89
// replace shorthand placeholders (which are convenient
90
// when writing a Caddyfile) with their actual placeholder
91
// identifiers or variable names
@@ -172,6 +179,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
179
result.directive = dir
180
sb.pile[result.Class] = append(sb.pile[result.Class], result)
181
}
182
+
183
+ // specially handle named routes that were pulled out from
184
+ // the invoke directive, which could be nested anywhere within
185
+ // some subroutes in this directive; we add them to the pile
186
+ // for this server block
187
+ if state[namedRouteKey] != nil {
188
+ for name := range state[namedRouteKey].(map[string]struct{}) {
189
+ result := ConfigValue{Class: namedRouteKey, Value: name}
190
+ sb.pile[result.Class] = append(sb.pile[result.Class], result)
191
+ }
192
+ state[namedRouteKey] = nil
193
+ }
194
}
195
}
196
@@ -403,6 +422,77 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
422
return serverBlocks[1:], nil
423
}
424
425
+// extractNamedRoutes pulls out any named route server blocks
426
+// so they don't get parsed as sites, and stores them in options
427
+// for later.
428
+func (ServerType) extractNamedRoutes(
429
+ serverBlocks []serverBlock,
430
+ options map[string]any,
431
+ warnings *[]caddyconfig.Warning,
432
+) ([]serverBlock, error) {
433
+ namedRoutes := map[string]*caddyhttp.Route{}
434
+
435
+ gc := counter{new(int)}
436
+ state := make(map[string]any)
437
+
438
+ // copy the server blocks so we can
439
+ // splice out the named route ones
440
+ filtered := append([]serverBlock{}, serverBlocks...)
441
+ index := -1
442
+
443
+ for _, sb := range serverBlocks {
444
+ index++
445
+ if !sb.block.IsNamedRoute {
446
+ continue
447
+ }
448
+
449
+ // splice out this block, because we know it's not a real server
450
+ filtered = append(filtered[:index], filtered[index+1:]...)
451
+ index--
452
+
453
+ if len(sb.block.Segments) == 0 {
454
+ continue
455
+ }
456
+
457
+ // zip up all the segments since ParseSegmentAsSubroute
458
+ // was designed to take a directive+
459
+ wholeSegment := caddyfile.Segment{}
460
+ for _, segment := range sb.block.Segments {
461
+ wholeSegment = append(wholeSegment, segment...)
462
+ }
463
+
464
+ h := Helper{
465
+ Dispenser: caddyfile.NewDispenser(wholeSegment),
466
+ options: options,
467
+ warnings: warnings,
468
+ matcherDefs: nil,
469
+ parentBlock: sb.block,
470
+ groupCounter: gc,
471
+ State: state,
472
+ }
473
+
474
+ handler, err := ParseSegmentAsSubroute(h)
475
+ if err != nil {
476
+ return nil, err
477
+ }
478
+ subroute := handler.(*caddyhttp.Subroute)
479
+ route := caddyhttp.Route{}
480
+
481
+ if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
482
+ // if there's only one route with no matcher, then we can simplify
483
+ route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
484
+ } else {
485
+ // otherwise we need the whole subroute
486
+ route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
487
+ }
488
+
489
+ namedRoutes[sb.block.Keys[0]] = &route
490
+ }
491
+ options["named_routes"] = namedRoutes
492
+
493
+ return filtered, nil
494
+}
495
+
496
// serversFromPairings creates the servers for each pairing of addresses
497
// to server blocks. Each pairing is essentially a server definition.
498
func (st *ServerType) serversFromPairings(
@@ -542,6 +632,24 @@ func (st *ServerType) serversFromPairings(
632
}
633
}
634
635
+ // add named routes to the server if 'invoke' was used inside of it
636
+ configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
637
+ for _, sblock := range p.serverBlocks {
638
+ if len(sblock.pile[namedRouteKey]) == 0 {
639
+ continue
640
+ }
641
+ for _, value := range sblock.pile[namedRouteKey] {
642
+ if srv.NamedRoutes == nil {
643
+ srv.NamedRoutes = map[string]*caddyhttp.Route{}
644
+ }
645
+ name := value.Value.(string)
646
+ if configuredNamedRoutes[name] == nil {
647
+ return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
648
+ }
649
+ srv.NamedRoutes[name] = configuredNamedRoutes[name]
650
+ }
651
+ }
652
+
653
// create a subroute for each site in the server block
654
for _, sblock := range p.serverBlocks {
655
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -1469,6 +1577,7 @@ type sbAddrAssociation struct {
1577
}
1578
1579
const matcherPrefix = "@"
1580
+const namedRouteKey = "named_route"
1581
1582
// Interface guard
1583
var _ caddyfile.ServerType = (*ServerType)(nil)
caddytest/integration/caddyfile_adapt/invoke_named_routes.txt
new
+154
@@ -0,0 +1,154 @@
1
+&(first) {
2
+ @first path /first
3
+ vars @first first 1
4
+ respond "first"
5
+}
6
+
7
+&(second) {
8
+ respond "second"
9
+}
10
+
11
+:8881 {
12
+ invoke first
13
+ route {
14
+ invoke second
15
+ }
16
+}
17
+
18
+:8882 {
19
+ handle {
20
+ invoke second
21
+ }
22
+}
23
+
24
+:8883 {
25
+ respond "no invoke"
26
+}
27
+----------
28
+{
29
+ "apps": {
30
+ "http": {
31
+ "servers": {
32
+ "srv0": {
33
+ "listen": [
34
+ ":8881"
35
+ ],
36
+ "routes": [
37
+ {
38
+ "handle": [
39
+ {
40
+ "handler": "invoke",
41
+ "name": "first"
42
+ },
43
+ {
44
+ "handler": "subroute",
45
+ "routes": [
46
+ {
47
+ "handle": [
48
+ {
49
+ "handler": "invoke",
50
+ "name": "second"
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ ],
59
+ "named_routes": {
60
+ "first": {
61
+ "handle": [
62
+ {
63
+ "handler": "subroute",
64
+ "routes": [
65
+ {
66
+ "handle": [
67
+ {
68
+ "first": 1,
69
+ "handler": "vars"
70
+ }
71
+ ],
72
+ "match": [
73
+ {
74
+ "path": [
75
+ "/first"
76
+ ]
77
+ }
78
+ ]
79
+ },
80
+ {
81
+ "handle": [
82
+ {
83
+ "body": "first",
84
+ "handler": "static_response"
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ },
92
+ "second": {
93
+ "handle": [
94
+ {
95
+ "body": "second",
96
+ "handler": "static_response"
97
+ }
98
+ ]
99
+ }
100
+ }
101
+ },
102
+ "srv1": {
103
+ "listen": [
104
+ ":8882"
105
+ ],
106
+ "routes": [
107
+ {
108
+ "handle": [
109
+ {
110
+ "handler": "subroute",
111
+ "routes": [
112
+ {
113
+ "handle": [
114
+ {
115
+ "handler": "invoke",
116
+ "name": "second"
117
+ }
118
+ ]
119
+ }
120
+ ]
121
+ }
122
+ ]
123
+ }
124
+ ],
125
+ "named_routes": {
126
+ "second": {
127
+ "handle": [
128
+ {
129
+ "body": "second",
130
+ "handler": "static_response"
131
+ }
132
+ ]
133
+ }
134
+ }
135
+ },
136
+ "srv2": {
137
+ "listen": [
138
+ ":8883"
139
+ ],
140
+ "routes": [
141
+ {
142
+ "handle": [
143
+ {
144
+ "body": "no invoke",
145
+ "handler": "static_response"
146
+ }
147
+ ]
148
+ }
149
+ ]
150
+ }
151
+ }
152
+ }
153
+ }
154
+}
\ No newline at end of file
modules/caddyhttp/app.go
+9
-1
@@ -293,11 +293,19 @@ func (app *App) Provision(ctx caddy.Context) error {
293
if srv.Errors != nil {
294
err := srv.Errors.Routes.Provision(ctx)
295
if err != nil {
296
- return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
296
+ return fmt.Errorf("server %s: setting up error handling routes: %v", srvName, err)
297
}
298
srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler)
299
}
300
301
+ // provision the named routes (they get compiled at runtime)
302
+ for name, route := range srv.NamedRoutes {
303
+ err := route.Provision(ctx, srv.Metrics)
304
+ if err != nil {
305
+ return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err)
306
+ }
307
+ }
308
+
309
// prepare the TLS connection policies
310
err = srv.TLSConnPolicies.Provision(ctx)
311
if err != nil {
modules/caddyhttp/invoke.go
new
+56
@@ -0,0 +1,56 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddyhttp
16
+
17
+import (
18
+ "fmt"
19
+ "net/http"
20
+
21
+ "github.com/caddyserver/caddy/v2"
22
+)
23
+
24
+func init() {
25
+ caddy.RegisterModule(Invoke{})
26
+}
27
+
28
+// Invoke implements a handler that compiles and executes a
29
+// named route that was defined on the server.
30
+//
31
+// EXPERIMENTAL: Subject to change or removal.
32
+type Invoke struct {
33
+ // Name is the key of the named route to execute
34
+ Name string `json:"name,omitempty"`
35
+}
36
+
37
+// CaddyModule returns the Caddy module information.
38
+func (Invoke) CaddyModule() caddy.ModuleInfo {
39
+ return caddy.ModuleInfo{
40
+ ID: "http.handlers.invoke",
41
+ New: func() caddy.Module { return new(Invoke) },
42
+ }
43
+}
44
+
45
+func (invoke *Invoke) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
46
+ server := r.Context().Value(ServerCtxKey).(*Server)
47
+ if route, ok := server.NamedRoutes[invoke.Name]; ok {
48
+ return route.Compile(next).ServeHTTP(w, r)
49
+ }
50
+ return fmt.Errorf("invoke: route '%s' not found", invoke.Name)
51
+}
52
+
53
+// Interface guards
54
+var (
55
+ _ MiddlewareHandler = (*Invoke)(nil)
56
+)
modules/caddyhttp/routes.go
+59
-18
@@ -120,6 +120,59 @@ func (r Route) String() string {
120
r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal)
121
}
122
123
+// Provision sets up both the matchers and handlers in the route.
124
+func (r *Route) Provision(ctx caddy.Context, metrics *Metrics) error {
125
+ err := r.ProvisionMatchers(ctx)
126
+ if err != nil {
127
+ return err
128
+ }
129
+ return r.ProvisionHandlers(ctx, metrics)
130
+}
131
+
132
+// ProvisionMatchers sets up all the matchers by loading the
133
+// matcher modules. Only call this method directly if you need
134
+// to set up matchers and handlers separately without having
135
+// to provision a second time; otherwise use Provision instead.
136
+func (r *Route) ProvisionMatchers(ctx caddy.Context) error {
137
+ // matchers
138
+ matchersIface, err := ctx.LoadModule(r, "MatcherSetsRaw")
139
+ if err != nil {
140
+ return fmt.Errorf("loading matcher modules: %v", err)
141
+ }
142
+ err = r.MatcherSets.FromInterface(matchersIface)
143
+ if err != nil {
144
+ return err
145
+ }
146
+ return nil
147
+}
148
+
149
+// ProvisionHandlers sets up all the handlers by loading the
150
+// handler modules. Only call this method directly if you need
151
+// to set up matchers and handlers separately without having
152
+// to provision a second time; otherwise use Provision instead.
153
+func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
154
+ handlersIface, err := ctx.LoadModule(r, "HandlersRaw")
155
+ if err != nil {
156
+ return fmt.Errorf("loading handler modules: %v", err)
157
+ }
158
+ for _, handler := range handlersIface.([]any) {
159
+ r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
160
+ }
161
+
162
+ // pre-compile the middleware handler chain
163
+ for _, midhandler := range r.Handlers {
164
+ r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))
165
+ }
166
+ return nil
167
+}
168
+
169
+// Compile prepares a middleware chain from the route list.
170
+// This should only be done once during the request, just
171
+// before the middleware chain is executed.
172
+func (r Route) Compile(next Handler) Handler {
173
+ return wrapRoute(r)(next)
174
+}
175
+
176
// RouteList is a list of server routes that can
177
// create a middleware chain.
178
type RouteList []Route
@@ -139,12 +192,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error {
192
// to provision a second time; otherwise use Provision instead.
193
func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error {
194
for i := range routes {
142
- // matchers
143
- matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw")
144
- if err != nil {
145
- return fmt.Errorf("route %d: loading matcher modules: %v", i, err)
146
- }
147
- err = routes[i].MatcherSets.FromInterface(matchersIface)
195
+ err := routes[i].ProvisionMatchers(ctx)
196
if err != nil {
197
return fmt.Errorf("route %d: %v", i, err)
198
}
@@ -158,25 +206,18 @@ func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error {
206
// to provision a second time; otherwise use Provision instead.
207
func (routes RouteList) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
208
for i := range routes {
161
- handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw")
209
+ err := routes[i].ProvisionHandlers(ctx, metrics)
210
if err != nil {
163
- return fmt.Errorf("route %d: loading handler modules: %v", i, err)
164
- }
165
- for _, handler := range handlersIface.([]any) {
166
- routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler))
167
- }
168
-
169
- // pre-compile the middleware handler chain
170
- for _, midhandler := range routes[i].Handlers {
171
- routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler, metrics))
211
+ return fmt.Errorf("route %d: %v", i, err)
212
}
213
}
214
return nil
215
}
216
217
// Compile prepares a middleware chain from the route list.
178
-// This should only be done once: after all the routes have
179
-// been provisioned, and before serving requests.
218
+// This should only be done either once during provisioning
219
+// for top-level routes, or on each request just before the
220
+// middleware chain is executed for subroutes.
221
func (routes RouteList) Compile(next Handler) Handler {
222
mid := make([]Middleware, 0, len(routes))
223
for _, route := range routes {
modules/caddyhttp/server.go
+10
@@ -102,6 +102,16 @@ type Server struct {
102
// The error routes work exactly like the normal routes.
103
Errors *HTTPErrorConfig `json:"errors,omitempty"`
104
105
+ // NamedRoutes describes a mapping of reusable routes that can be
106
+ // invoked by their name. This can be used to optimize memory usage
107
+ // when the same route is needed for many subroutes, by having
108
+ // the handlers and matchers be only provisioned once, but used from
109
+ // many places. These routes are not executed unless they are invoked
110
+ // from another route.
111
+ //
112
+ // EXPERIMENTAL: Subject to change or removal.
113
+ NamedRoutes map[string]*Route `json:"named_routes,omitempty"`
114
+
115
// How to handle TLS connections. At least one policy is
116
// required to enable HTTPS on this server if automatic
117
// HTTPS is disabled or does not apply.