Partial support for location contexts in config files

This commit is contained in:
Matthew Holt 2015-03-03 09:49:01 -07:00
parent 634b8b707f
commit 974acbf38c
8 changed files with 113 additions and 34 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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() {

View File

@ -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 + "'")

View File

@ -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
}

View File

@ -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()

View File

@ -29,5 +29,6 @@ type (
Root() string
Host() string
Port() string
Context() Path
}
)

View File

@ -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
}