From cbf16f6d9eb77f37d6eb588ff3e54cfdfddecc21 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 16 May 2023 11:27:52 -0400 Subject: caddyhttp: Implement named routes, `invoke` directive (#5107) * caddyhttp: Implement named routes, `invoke` directive * gofmt * Add experimental marker * Adjust route compile comments --- modules/caddyhttp/app.go | 10 +++++- modules/caddyhttp/invoke.go | 56 +++++++++++++++++++++++++++++++++ modules/caddyhttp/routes.go | 77 ++++++++++++++++++++++++++++++++++----------- modules/caddyhttp/server.go | 10 ++++++ 4 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 modules/caddyhttp/invoke.go (limited to 'modules') diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 53b5782..0e02afd 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -293,11 +293,19 @@ func (app *App) Provision(ctx caddy.Context) error { if srv.Errors != nil { err := srv.Errors.Routes.Provision(ctx) if err != nil { - return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err) + return fmt.Errorf("server %s: setting up error handling routes: %v", srvName, err) } srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler) } + // provision the named routes (they get compiled at runtime) + for name, route := range srv.NamedRoutes { + err := route.Provision(ctx, srv.Metrics) + if err != nil { + return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err) + } + } + // prepare the TLS connection policies err = srv.TLSConnPolicies.Provision(ctx) if err != nil { diff --git a/modules/caddyhttp/invoke.go b/modules/caddyhttp/invoke.go new file mode 100644 index 0000000..97fd1cc --- /dev/null +++ b/modules/caddyhttp/invoke.go @@ -0,0 +1,56 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyhttp + +import ( + "fmt" + "net/http" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(Invoke{}) +} + +// Invoke implements a handler that compiles and executes a +// named route that was defined on the server. +// +// EXPERIMENTAL: Subject to change or removal. +type Invoke struct { + // Name is the key of the named route to execute + Name string `json:"name,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (Invoke) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.handlers.invoke", + New: func() caddy.Module { return new(Invoke) }, + } +} + +func (invoke *Invoke) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { + server := r.Context().Value(ServerCtxKey).(*Server) + if route, ok := server.NamedRoutes[invoke.Name]; ok { + return route.Compile(next).ServeHTTP(w, r) + } + return fmt.Errorf("invoke: route '%s' not found", invoke.Name) +} + +// Interface guards +var ( + _ MiddlewareHandler = (*Invoke)(nil) +) diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index da25097..9be3d01 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -120,6 +120,59 @@ func (r Route) String() string { r.Group, r.MatcherSetsRaw, handlersRaw, r.Terminal) } +// Provision sets up both the matchers and handlers in the route. +func (r *Route) Provision(ctx caddy.Context, metrics *Metrics) error { + err := r.ProvisionMatchers(ctx) + if err != nil { + return err + } + return r.ProvisionHandlers(ctx, metrics) +} + +// ProvisionMatchers sets up all the matchers by loading the +// matcher modules. Only call this method directly if you need +// to set up matchers and handlers separately without having +// to provision a second time; otherwise use Provision instead. +func (r *Route) ProvisionMatchers(ctx caddy.Context) error { + // matchers + matchersIface, err := ctx.LoadModule(r, "MatcherSetsRaw") + if err != nil { + return fmt.Errorf("loading matcher modules: %v", err) + } + err = r.MatcherSets.FromInterface(matchersIface) + if err != nil { + return err + } + return nil +} + +// ProvisionHandlers sets up all the handlers by loading the +// handler modules. Only call this method directly if you need +// to set up matchers and handlers separately without having +// to provision a second time; otherwise use Provision instead. +func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error { + handlersIface, err := ctx.LoadModule(r, "HandlersRaw") + if err != nil { + return fmt.Errorf("loading handler modules: %v", err) + } + for _, handler := range handlersIface.([]any) { + r.Handlers = append(r.Handlers, handler.(MiddlewareHandler)) + } + + // pre-compile the middleware handler chain + for _, midhandler := range r.Handlers { + r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics)) + } + return nil +} + +// Compile prepares a middleware chain from the route list. +// This should only be done once during the request, just +// before the middleware chain is executed. +func (r Route) Compile(next Handler) Handler { + return wrapRoute(r)(next) +} + // RouteList is a list of server routes that can // create a middleware chain. type RouteList []Route @@ -139,12 +192,7 @@ func (routes RouteList) Provision(ctx caddy.Context) error { // to provision a second time; otherwise use Provision instead. func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error { for i := range routes { - // matchers - matchersIface, err := ctx.LoadModule(&routes[i], "MatcherSetsRaw") - if err != nil { - return fmt.Errorf("route %d: loading matcher modules: %v", i, err) - } - err = routes[i].MatcherSets.FromInterface(matchersIface) + err := routes[i].ProvisionMatchers(ctx) if err != nil { return fmt.Errorf("route %d: %v", i, err) } @@ -158,25 +206,18 @@ func (routes RouteList) ProvisionMatchers(ctx caddy.Context) error { // to provision a second time; otherwise use Provision instead. func (routes RouteList) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error { for i := range routes { - handlersIface, err := ctx.LoadModule(&routes[i], "HandlersRaw") + err := routes[i].ProvisionHandlers(ctx, metrics) if err != nil { - return fmt.Errorf("route %d: loading handler modules: %v", i, err) - } - for _, handler := range handlersIface.([]any) { - routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler)) - } - - // pre-compile the middleware handler chain - for _, midhandler := range routes[i].Handlers { - routes[i].middleware = append(routes[i].middleware, wrapMiddleware(ctx, midhandler, metrics)) + return fmt.Errorf("route %d: %v", i, err) } } return nil } // Compile prepares a middleware chain from the route list. -// This should only be done once: after all the routes have -// been provisioned, and before serving requests. +// This should only be done either once during provisioning +// for top-level routes, or on each request just before the +// middleware chain is executed for subroutes. func (routes RouteList) Compile(next Handler) Handler { mid := make([]Middleware, 0, len(routes)) for _, route := range routes { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 411ec72..d2de09b 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -102,6 +102,16 @@ type Server struct { // The error routes work exactly like the normal routes. Errors *HTTPErrorConfig `json:"errors,omitempty"` + // NamedRoutes describes a mapping of reusable routes that can be + // invoked by their name. This can be used to optimize memory usage + // when the same route is needed for many subroutes, by having + // the handlers and matchers be only provisioned once, but used from + // many places. These routes are not executed unless they are invoked + // from another route. + // + // EXPERIMENTAL: Subject to change or removal. + NamedRoutes map[string]*Route `json:"named_routes,omitempty"` + // How to handle TLS connections. At least one policy is // required to enable HTTPS on this server if automatic // HTTPS is disabled or does not apply. -- cgit v1.2.3