From 8c5d00b2bc815c182e1a510be6dddc128949bf23 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Tue, 26 May 2020 17:27:51 -0400 Subject: 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 --- caddyconfig/httpcaddyfile/builtins.go | 4 +- caddyconfig/httpcaddyfile/directives.go | 115 +++++++++++---------- .../integration/caddyfile_adapt/handle_path.txt | 52 ++++++++++ modules/caddyhttp/rewrite/caddyfile.go | 74 +++++++++++++ 4 files changed, 186 insertions(+), 59 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/handle_path.txt diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 2bb9b90..7026dfe 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -442,11 +442,11 @@ func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) { } func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) { - return parseSegmentAsSubroute(h) + return ParseSegmentAsSubroute(h) } func parseHandleErrors(h Helper) ([]ConfigValue, error) { - subroute, err := parseSegmentAsSubroute(h) + subroute, err := ParseSegmentAsSubroute(h) if err != nil { return nil, err } diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 1578772..cde9743 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -57,6 +57,7 @@ var directiveOrder = []string{ // special routing directives "handle", "route", + "handle_path", // handlers that typically respond to requests "respond", @@ -261,6 +262,63 @@ func (h Helper) NewBindAddresses(addrs []string) []ConfigValue { return []ConfigValue{{Class: "bind", Value: addrs}} } +// ParseSegmentAsSubroute parses the segment such that its subdirectives +// are themselves treated as directives, from which a subroute is built +// and returned. +func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) { + var allResults []ConfigValue + + for h.Next() { + // slice the linear list of tokens into top-level segments + var segments []caddyfile.Segment + for nesting := h.Nesting(); h.NextBlock(nesting); { + segments = append(segments, h.NextSegment()) + } + + // copy existing matcher definitions so we can augment + // new ones that are defined only in this scope + matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs)) + for key, val := range h.matcherDefs { + matcherDefs[key] = val + } + + // find and extract any embedded matcher definitions in this scope + for i, seg := range segments { + if strings.HasPrefix(seg.Directive(), matcherPrefix) { + err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs) + if err != nil { + return nil, err + } + segments = append(segments[:i], segments[i+1:]...) + } + } + + // with matchers ready to go, evaluate each directive's segment + for _, seg := range segments { + dir := seg.Directive() + dirFunc, ok := registeredDirectives[dir] + if !ok { + return nil, h.Errf("unrecognized directive: %s", dir) + } + + subHelper := h + subHelper.Dispenser = caddyfile.NewDispenser(seg) + subHelper.matcherDefs = matcherDefs + + results, err := dirFunc(subHelper) + if err != nil { + return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err) + } + for _, result := range results { + result.directive = dir + allResults = append(allResults, result) + } + } + } + + return buildSubroute(allResults, h.groupCounter) +} + // ConfigValue represents a value to be added to the final // configuration, or a value to be consulted when building // the final configuration. @@ -329,63 +387,6 @@ func sortRoutes(routes []ConfigValue) { }) } -// parseSegmentAsSubroute parses the segment such that its subdirectives -// are themselves treated as directives, from which a subroute is built -// and returned. -func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) { - var allResults []ConfigValue - - for h.Next() { - // slice the linear list of tokens into top-level segments - var segments []caddyfile.Segment - for nesting := h.Nesting(); h.NextBlock(nesting); { - segments = append(segments, h.NextSegment()) - } - - // copy existing matcher definitions so we can augment - // new ones that are defined only in this scope - matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs)) - for key, val := range h.matcherDefs { - matcherDefs[key] = val - } - - // find and extract any embedded matcher definitions in this scope - for i, seg := range segments { - if strings.HasPrefix(seg.Directive(), matcherPrefix) { - err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs) - if err != nil { - return nil, err - } - segments = append(segments[:i], segments[i+1:]...) - } - } - - // with matchers ready to go, evaluate each directive's segment - for _, seg := range segments { - dir := seg.Directive() - dirFunc, ok := registeredDirectives[dir] - if !ok { - return nil, h.Errf("unrecognized directive: %s", dir) - } - - subHelper := h - subHelper.Dispenser = caddyfile.NewDispenser(seg) - subHelper.matcherDefs = matcherDefs - - results, err := dirFunc(subHelper) - if err != nil { - return nil, h.Errf("parsing caddyfile tokens for '%s': %v", dir, err) - } - for _, result := range results { - result.directive = dir - allResults = append(allResults, result) - } - } - } - - return buildSubroute(allResults, h.groupCounter) -} - // serverBlock pairs a Caddyfile server block with // a "pile" of config values, keyed by class name, // as well as its parsed keys for convenience. diff --git a/caddytest/integration/caddyfile_adapt/handle_path.txt b/caddytest/integration/caddyfile_adapt/handle_path.txt new file mode 100644 index 0000000..7f40fcf --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/handle_path.txt @@ -0,0 +1,52 @@ +:80 +handle_path /api/v1/* { + respond "API v1" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "match": [ + { + "path": [ + "/api/v1/*" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "rewrite", + "strip_path_prefix": "/api/v1" + } + ] + }, + { + "handle": [ + { + "body": "API v1", + "handler": "static_response" + } + ] + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go index ee7dd23..950119d 100644 --- a/modules/caddyhttp/rewrite/caddyfile.go +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -15,9 +15,12 @@ package rewrite import ( + "encoding/json" "strconv" "strings" + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) @@ -25,6 +28,7 @@ import ( func init() { httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfileRewrite) httpcaddyfile.RegisterHandlerDirective("uri", parseCaddyfileURI) + httpcaddyfile.RegisterDirective("handle_path", parseCaddyfileHandlePath) } // parseCaddyfileRewrite sets up a basic rewrite handler from Caddyfile tokens. Syntax: @@ -110,3 +114,73 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err } return rewr, nil } + +// parseCaddyfileHandlePath parses the handle_path directive. Syntax: +// +// handle_path [] { +// +// } +// +// Only path matchers (with a `/` prefix) are supported as this is a shortcut +// for the handle directive with a strip_prefix rewrite. +func parseCaddyfileHandlePath(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + if !h.NextArg() { + return nil, h.ArgErr() + } + + // read the prefix to strip + path := h.Val() + if !strings.HasPrefix(path, "/") { + return nil, h.Errf("path matcher must begin with '/', got %s", path) + } + + // we only want to strip what comes before the '/' if + // the user specified it (e.g. /api/* should only strip /api) + var stripPath string + if strings.HasSuffix(path, "/*") { + stripPath = path[:len(path)-2] + } else if strings.HasSuffix(path, "*") { + stripPath = path[:len(path)-1] + } else { + stripPath = path + } + + // the ParseSegmentAsSubroute function expects the cursor + // to be at the token just before the block opening, + // so we need to rewind because we already read past it + h.Reset() + h.Next() + + // parse the block contents as a subroute handler + handler, err := httpcaddyfile.ParseSegmentAsSubroute(h) + if err != nil { + return nil, err + } + subroute, ok := handler.(*caddyhttp.Subroute) + if !ok { + return nil, h.Errf("segment was not parsed as a subroute") + } + + // make a matcher on the path and everything below it + pathMatcher := caddy.ModuleMap{ + "path": h.JSON(caddyhttp.MatchPath{path}), + } + + // build a route with a rewrite handler to strip the path prefix + route := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(Rewrite{ + StripPathPrefix: stripPath, + }, "handler", "rewrite", nil), + }, + } + + // prepend the route to the subroute + subroute.Routes = append([]caddyhttp.Route{route}, subroute.Routes...) + + // build and return a route from the subroute + return h.NewRoute(pathMatcher, subroute), nil +} -- cgit v1.2.3