@marcel91200 / caddy Mirror · read-only / commits / cbf16f6d
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 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.