From 974acbf38c426c2273c3d3b2520d29a4e8ad797f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 3 Mar 2015 09:49:01 -0700 Subject: [PATCH] Partial support for location contexts in config files --- config/config.go | 2 +- config/controller.go | 9 ++++- config/directives.go | 4 ++- config/parser.go | 52 +++++++++++++++++++--------- config/parsing.go | 69 +++++++++++++++++++++++++++++-------- middleware/headers/parse.go | 4 +++ middleware/middleware.go | 1 + server/server.go | 6 +++- 8 files changed, 113 insertions(+), 34 deletions(-) diff --git a/config/config.go b/config/config.go index 659fb5933..9b6d5f5f3 100644 --- a/config/config.go +++ b/config/config.go @@ -63,7 +63,7 @@ type Config struct { Port string Root string TLS TLSConfig - Middleware []middleware.Middleware + Middleware map[string][]middleware.Middleware Startup []func() error MaxCPU int } diff --git a/config/controller.go b/config/controller.go index 4f7cb8d64..20a289940 100644 --- a/config/controller.go +++ b/config/controller.go @@ -1,12 +1,15 @@ package config +import "github.com/mholt/caddy/middleware" + // controller is a dispenser of tokens and also // facilitates setup with the server by providing // access to its configuration. It implements // the middleware.Controller interface. type controller struct { dispenser - parser *parser + parser *parser + pathScope string } // newController returns a new controller. @@ -43,3 +46,7 @@ func (c *controller) Host() string { func (c *controller) Port() string { return c.parser.cfg.Port } + +func (c *controller) Context() middleware.Path { + return middleware.Path(c.pathScope) +} diff --git a/config/directives.go b/config/directives.go index 26047ddf9..c5c9453f3 100644 --- a/config/directives.go +++ b/config/directives.go @@ -12,7 +12,9 @@ import ( type dirFunc func(*parser) error // validDirectives is a map of valid, built-in directive names -// to their parsing function. +// to their parsing function. Built-in directives cannot be +// ordered, so they should only be used for internal server +// configuration; not directly handling requests. var validDirectives map[string]dirFunc func init() { diff --git a/config/parser.go b/config/parser.go index 52df1d974..52b43a058 100644 --- a/config/parser.go +++ b/config/parser.go @@ -5,16 +5,30 @@ import ( "fmt" "os" "strings" + + "github.com/mholt/caddy/middleware" ) -// parser is a type which can parse config files. -type parser struct { - filename string // the name of the file that we're parsing - lexer lexer // the lexer that is giving us tokens from the raw input - cfg Config // each server gets one Config; this is the one we're currently building - other map[string]*controller // tokens to be parsed later by others (middleware generators) - unused bool // sometimes the token won't be immediately consumed -} +type ( + // parser is a type which can parse config files. + parser struct { + filename string // the name of the file that we're parsing + lexer lexer // the lexer that is giving us tokens from the raw input + cfg Config // each server gets one Config; this is the one we're currently building + other []locationContext // tokens to be 'parsed' later by middleware generators + scope *locationContext // the current location context (path scope) being populated + unused bool // sometimes a token will be read but not immediately consumed + } + + // locationContext represents a location context + // (path block) in a config file. If no context + // is explicitly defined, the default location + // context is "/". + locationContext struct { + path string + directives map[string]*controller + } +) // newParser makes a new parser and prepares it for parsing, given // the input to parse. @@ -78,13 +92,14 @@ func (p *parser) next() bool { // file for a single Config object (each server or // virtualhost instance gets their own Config struct), // which is until the next address/server block. -// Call this only after you know that the lexer has another -// another token and you're not in the middle of a server +// Call this only when you know that the lexer has another +// another token and you're not in another server // block already. func (p *parser) parseOne() error { - p.cfg = Config{} - - p.other = make(map[string]*controller) + p.cfg = Config{ + Middleware: make(map[string][]middleware.Middleware), + } + p.other = []locationContext{} err := p.begin() if err != nil { @@ -102,19 +117,24 @@ func (p *parser) parseOne() error { // unwrap gets the middleware generators from the middleware // package in the order in which they are registered, and // executes the top-level functions (the generator function) -// to expose the second layers which is the actual middleware. +// to expose the second layers which are the actual middleware. // This function should be called only after p has filled out // p.other and that the entire server block has been consumed. func (p *parser) unwrap() error { for _, directive := range registry.ordered { - if disp, ok := p.other[directive]; ok { + // TODO: For now, we only support the first and default path scope ("/") + // but when we implement support for path scopes, we will have to + // change this logic to loop over them and order them. We need to account + // for situations where multiple path scopes overlap, regex (??), etc... + if disp, ok := p.other[0].directives[directive]; ok { if generator, ok := registry.directiveMap[directive]; ok { mid, err := generator(disp) if err != nil { return err } if mid != nil { - p.cfg.Middleware = append(p.cfg.Middleware, mid) + // TODO: Again, we assume the default path scope here... + p.cfg.Middleware[p.other[0].path] = append(p.cfg.Middleware[p.other[0].path], mid) } } else { return errors.New("No middleware bound to directive '" + directive + "'") diff --git a/config/parsing.go b/config/parsing.go index 8e85184aa..c10af532c 100644 --- a/config/parsing.go +++ b/config/parsing.go @@ -1,5 +1,7 @@ package config +import "errors" + // This file contains the recursive-descent parsing // functions. @@ -47,6 +49,14 @@ func (p *parser) addressBlock() error { return p.directives() } + // When we enter an address block, we also implicitly + // enter a path block where the path is all paths ("/") + p.other = append(p.other, locationContext{ + path: "/", + directives: make(map[string]*controller), + }) + p.scope = &p.other[0] + err = p.directives() if err != nil { return err @@ -91,40 +101,66 @@ func (p *parser) directives() error { // end of address scope break } - if p.tkn()[0] == '/' { + if p.tkn()[0] == '/' || p.tkn()[0] == '*' { // Path scope (a.k.a. location context) + // Starts with / ('starts with') or * ('ends with'). + // TODO: The parser can handle the syntax (obviously), but the // implementation is incomplete. This is intentional, // until we can better decide what kind of feature set we - // want to support. Until this is ready, we leave this - // syntax undocumented. + // want to support and how exactly we want these location + // scopes to work. Until this is ready, we leave this + // syntax undocumented. Some changes will need to be + // made in parser.go also (the unwrap function) and + // probably in server.go when we do this... see those TODOs. - // location := p.tkn() + var scope *locationContext + // If the path block is a duplicate, append to existing one + for i := 0; i < len(p.other); i++ { + if p.other[i].path == p.tkn() { + scope = &p.other[i] + break + } + } + + // Otherwise, for a new path we haven't seen before, create a new context + if scope == nil { + scope = &locationContext{ + path: p.tkn(), + directives: make(map[string]*controller), + } + } + + // Consume the opening curly brace if !p.next() { return p.eofErr() } - err := p.openCurlyBrace() if err != nil { return err } + // Use this path scope as our current context for just a moment + p.scope = scope + + // Consume each directive in the path block for p.next() { err := p.closeCurlyBrace() - if err == nil { // end of location context + if err == nil { break } - // TODO: How should we give the context to the directives? - // Or how do we tell the server that these directives should only - // be executed for requests routed to the current path? - err = p.directive() if err != nil { return err } } + + // Save the new scope and put the current scope back to "/" + p.other = append(p.other, *scope) + p.scope = &p.other[0] + } else if err := p.directive(); err != nil { return err } @@ -134,10 +170,11 @@ func (p *parser) directives() error { // directive asserts that the current token is either a built-in // directive or a registered middleware directive; otherwise an error -// will be returned. +// will be returned. If it is a valid directive, tokens will be +// collected. func (p *parser) directive() error { if fn, ok := validDirectives[p.tkn()]; ok { - // Built-in (standard) directive + // Built-in (standard, or 'core') directive err := fn(p) if err != nil { return err @@ -159,6 +196,10 @@ func (p *parser) directive() error { // It creates a controller which is stored in the parser for // later use by the middleware. func (p *parser) collectTokens() error { + if p.scope == nil { + return errors.New("Current scope cannot be nil") + } + directive := p.tkn() line := p.line() nesting := 0 @@ -169,7 +210,7 @@ func (p *parser) collectTokens() error { // (the parsing logic in the middleware generator must // account for multiple occurrences of its directive, even // if that means returning an error or overwriting settings) - if existing, ok := p.other[directive]; ok { + if existing, ok := p.scope.directives[directive]; ok { cont = existing } @@ -195,6 +236,6 @@ func (p *parser) collectTokens() error { return p.eofErr() } - p.other[directive] = cont + p.scope.directives[directive] = cont return nil } diff --git a/middleware/headers/parse.go b/middleware/headers/parse.go index 67f657015..cfdf14b41 100644 --- a/middleware/headers/parse.go +++ b/middleware/headers/parse.go @@ -29,6 +29,8 @@ func parse(c middleware.Controller) ([]HeaderRule, error) { } for c.NextBlock() { + // A block of headers was opened... + h := Header{Name: c.Val()} if c.NextArg() { @@ -38,6 +40,8 @@ func parse(c middleware.Controller) ([]HeaderRule, error) { head.Headers = append(head.Headers, h) } if c.NextArg() { + // ... or single header was defined as an argument instead. + h := Header{Name: c.Val()} h.Value = c.Val() diff --git a/middleware/middleware.go b/middleware/middleware.go index 6613c824d..041c34b99 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -29,5 +29,6 @@ type ( Root() string Host() string Port() string + Context() Path } ) diff --git a/server/server.go b/server/server.go index 076eca2fe..54f5048a5 100644 --- a/server/server.go +++ b/server/server.go @@ -101,7 +101,11 @@ func (s *Server) buildStack() error { } } - s.compile(s.config.Middleware) + // TODO: We only compile middleware for the "/" scope. + // Partial support for multiple location contexts already + // exists in the parser and config levels, but until full + // support is implemented, this is all we do right here. + s.compile(s.config.Middleware["/"]) return nil }