From 974acbf38c426c2273c3d3b2520d29a4e8ad797f Mon Sep 17 00:00:00 2001
From: Matthew Holt <Matthew.Holt+git@gmail.com>
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
 }