diff options
| author | Francis Lavoie <lavofr@gmail.com> | 2023-05-16 11:27:52 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-16 15:27:52 +0000 | 
| commit | cbf16f6d9eb77f37d6eb588ff3e54cfdfddecc21 (patch) | |
| tree | ef48ed58e9f0374df46cabecd7edf29c2be019e2 /caddyconfig | |
| parent | 13a37688dcdc1ffa8e9322dad0bffac0c0c9893a (diff) | |
caddyhttp: Implement named routes, `invoke` directive (#5107)
* caddyhttp: Implement named routes, `invoke` directive
* gofmt
* Add experimental marker
* Adjust route compile comments
Diffstat (limited to 'caddyconfig')
| -rw-r--r-- | caddyconfig/caddyfile/parse.go | 50 | ||||
| -rw-r--r-- | caddyconfig/httpcaddyfile/builtins.go | 22 | ||||
| -rw-r--r-- | caddyconfig/httpcaddyfile/directives.go | 1 | ||||
| -rw-r--r-- | caddyconfig/httpcaddyfile/httptype.go | 113 | 
4 files changed, 176 insertions, 10 deletions
diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index ab84086..64d1062 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -148,7 +148,6 @@ func (p *parser) begin() error {  	}  	err := p.addresses() -  	if err != nil {  		return err  	} @@ -159,6 +158,25 @@ func (p *parser) begin() error {  		return nil  	} +	if ok, name := p.isNamedRoute(); ok { +		// named routes only have one key, the route name +		p.block.Keys = []string{name} +		p.block.IsNamedRoute = true + +		// we just need a dummy leading token to ease parsing later +		nameToken := p.Token() +		nameToken.Text = name + +		// get all the tokens from the block, including the braces +		tokens, err := p.blockTokens(true) +		if err != nil { +			return err +		} +		tokens = append([]Token{nameToken}, tokens...) +		p.block.Segments = []Segment{tokens} +		return nil +	} +  	if ok, name := p.isSnippet(); ok {  		if p.definedSnippets == nil {  			p.definedSnippets = map[string][]Token{} @@ -167,7 +185,7 @@ func (p *parser) begin() error {  			return p.Errf("redeclaration of previously declared snippet %s", name)  		}  		// consume all tokens til matched close brace -		tokens, err := p.snippetTokens() +		tokens, err := p.blockTokens(false)  		if err != nil {  			return err  		} @@ -576,6 +594,15 @@ func (p *parser) closeCurlyBrace() error {  	return nil  } +func (p *parser) isNamedRoute() (bool, string) { +	keys := p.block.Keys +	// A named route block is a single key with parens, prefixed with &. +	if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") { +		return true, strings.TrimSuffix(keys[0][2:], ")") +	} +	return false, "" +} +  func (p *parser) isSnippet() (bool, string) {  	keys := p.block.Keys  	// A snippet block is a single key with parens. Nothing else qualifies. @@ -586,18 +613,24 @@ func (p *parser) isSnippet() (bool, string) {  }  // read and store everything in a block for later replay. -func (p *parser) snippetTokens() ([]Token, error) { -	// snippet must have curlies. +func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) { +	// block must have curlies.  	err := p.openCurlyBrace()  	if err != nil {  		return nil, err  	} -	nesting := 1 // count our own nesting in snippets +	nesting := 1 // count our own nesting  	tokens := []Token{} +	if retainCurlies { +		tokens = append(tokens, p.Token()) +	}  	for p.Next() {  		if p.Val() == "}" {  			nesting--  			if nesting == 0 { +				if retainCurlies { +					tokens = append(tokens, p.Token()) +				}  				break  			}  		} @@ -617,9 +650,10 @@ func (p *parser) snippetTokens() ([]Token, error) {  // head of the server block with tokens, which are  // grouped by segments.  type ServerBlock struct { -	HasBraces bool -	Keys      []string -	Segments  []Segment +	HasBraces    bool +	Keys         []string +	Segments     []Segment +	IsNamedRoute bool  }  // DispenseDirective returns a dispenser that contains diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 45da4a8..d6cdf84 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -48,6 +48,7 @@ func init() {  	RegisterHandlerDirective("route", parseRoute)  	RegisterHandlerDirective("handle", parseHandle)  	RegisterDirective("handle_errors", parseHandleErrors) +	RegisterHandlerDirective("invoke", parseInvoke)  	RegisterDirective("log", parseLog)  	RegisterHandlerDirective("skip_log", parseSkipLog)  } @@ -764,6 +765,27 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {  	}, nil  } +// parseInvoke parses the invoke directive. +func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) { +	h.Next() // consume directive +	if !h.NextArg() { +		return nil, h.ArgErr() +	} +	for h.Next() || h.NextBlock(0) { +		return nil, h.ArgErr() +	} + +	// remember that we're invoking this name +	// to populate the server with these named routes +	if h.State[namedRouteKey] == nil { +		h.State[namedRouteKey] = map[string]struct{}{} +	} +	h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{} + +	// return the handler +	return &caddyhttp.Invoke{Name: h.Val()}, nil +} +  // parseLog parses the log directive. Syntax:  //  //	log { diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index b8faa4a..1223013 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -65,6 +65,7 @@ var directiveOrder = []string{  	"templates",  	// special routing & dispatching directives +	"invoke",  	"handle",  	"handle_path",  	"route", diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 90c90ee..c7aeb94 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -52,8 +52,10 @@ type ServerType struct {  }  // Setup makes a config from the tokens. -func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, -	options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) { +func (st ServerType) Setup( +	inputServerBlocks []caddyfile.ServerBlock, +	options map[string]any, +) (*caddy.Config, []caddyconfig.Warning, error) {  	var warnings []caddyconfig.Warning  	gc := counter{new(int)}  	state := make(map[string]any) @@ -79,6 +81,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,  		return nil, warnings, err  	} +	originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings) +	if err != nil { +		return nil, warnings, err +	} +  	// replace shorthand placeholders (which are convenient  	// when writing a Caddyfile) with their actual placeholder  	// identifiers or variable names @@ -172,6 +179,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,  				result.directive = dir  				sb.pile[result.Class] = append(sb.pile[result.Class], result)  			} + +			// specially handle named routes that were pulled out from +			// the invoke directive, which could be nested anywhere within +			// some subroutes in this directive; we add them to the pile +			// for this server block +			if state[namedRouteKey] != nil { +				for name := range state[namedRouteKey].(map[string]struct{}) { +					result := ConfigValue{Class: namedRouteKey, Value: name} +					sb.pile[result.Class] = append(sb.pile[result.Class], result) +				} +				state[namedRouteKey] = nil +			}  		}  	} @@ -403,6 +422,77 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options  	return serverBlocks[1:], nil  } +// extractNamedRoutes pulls out any named route server blocks +// so they don't get parsed as sites, and stores them in options +// for later. +func (ServerType) extractNamedRoutes( +	serverBlocks []serverBlock, +	options map[string]any, +	warnings *[]caddyconfig.Warning, +) ([]serverBlock, error) { +	namedRoutes := map[string]*caddyhttp.Route{} + +	gc := counter{new(int)} +	state := make(map[string]any) + +	// copy the server blocks so we can +	// splice out the named route ones +	filtered := append([]serverBlock{}, serverBlocks...) +	index := -1 + +	for _, sb := range serverBlocks { +		index++ +		if !sb.block.IsNamedRoute { +			continue +		} + +		// splice out this block, because we know it's not a real server +		filtered = append(filtered[:index], filtered[index+1:]...) +		index-- + +		if len(sb.block.Segments) == 0 { +			continue +		} + +		// zip up all the segments since ParseSegmentAsSubroute +		// was designed to take a directive+ +		wholeSegment := caddyfile.Segment{} +		for _, segment := range sb.block.Segments { +			wholeSegment = append(wholeSegment, segment...) +		} + +		h := Helper{ +			Dispenser:    caddyfile.NewDispenser(wholeSegment), +			options:      options, +			warnings:     warnings, +			matcherDefs:  nil, +			parentBlock:  sb.block, +			groupCounter: gc, +			State:        state, +		} + +		handler, err := ParseSegmentAsSubroute(h) +		if err != nil { +			return nil, err +		} +		subroute := handler.(*caddyhttp.Subroute) +		route := caddyhttp.Route{} + +		if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 { +			// if there's only one route with no matcher, then we can simplify +			route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0]) +		} else { +			// otherwise we need the whole subroute +			route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)} +		} + +		namedRoutes[sb.block.Keys[0]] = &route +	} +	options["named_routes"] = namedRoutes + +	return filtered, nil +} +  // serversFromPairings creates the servers for each pairing of addresses  // to server blocks. Each pairing is essentially a server definition.  func (st *ServerType) serversFromPairings( @@ -542,6 +632,24 @@ func (st *ServerType) serversFromPairings(  			}  		} +		// add named routes to the server if 'invoke' was used inside of it +		configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route) +		for _, sblock := range p.serverBlocks { +			if len(sblock.pile[namedRouteKey]) == 0 { +				continue +			} +			for _, value := range sblock.pile[namedRouteKey] { +				if srv.NamedRoutes == nil { +					srv.NamedRoutes = map[string]*caddyhttp.Route{} +				} +				name := value.Value.(string) +				if configuredNamedRoutes[name] == nil { +					return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name) +				} +				srv.NamedRoutes[name] = configuredNamedRoutes[name] +			} +		} +  		// create a subroute for each site in the server block  		for _, sblock := range p.serverBlocks {  			matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) @@ -1469,6 +1577,7 @@ type sbAddrAssociation struct {  }  const matcherPrefix = "@" +const namedRouteKey = "named_route"  // Interface guard  var _ caddyfile.ServerType = (*ServerType)(nil)  | 
