Commit 24fc2ae5 authored by Matthew Holt's avatar Matthew Holt

Major refactoring; more modular middleware

parent 7b3d0056
...@@ -2,20 +2,30 @@ ...@@ -2,20 +2,30 @@
// launching specially-configured server instances. // launching specially-configured server instances.
package config package config
import "os" import (
"os"
"github.com/mholt/caddy/middleware"
)
const (
defaultHost = "localhost"
defaultPort = "8080"
defaultRoot = "."
)
// Load loads a configuration file, parses it, // Load loads a configuration file, parses it,
// and returns a slice of Config structs which // and returns a slice of Config structs which
// can be used to create and configure server // can be used to create and configure server
// instances. // instances.
func Load(filename string) ([]Config, error) { func Load(filename string) ([]Config, error) {
p := parser{} file, err := os.Open(filename)
err := p.lexer.Load(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer p.lexer.Close() defer file.Close()
return p.Parse() p := newParser(file)
return p.parse()
} }
// IsNotFound returns whether or not the error is // IsNotFound returns whether or not the error is
...@@ -41,21 +51,15 @@ func Default() []Config { ...@@ -41,21 +51,15 @@ func Default() []Config {
} }
// config represents a server configuration. It // config represents a server configuration. It
// is populated by parsing a config file. (Use // is populated by parsing a config file (via the
// the Load function.) // Load function).
type Config struct { type Config struct {
Host string Host string
Port string Port string
Root string Root string
Gzip bool
RequestLog Log
ErrorLog Log
Rewrites []Rewrite
Redirects []Redirect
Extensions []string
ErrorPages map[int]string // Map of HTTP status code to filename
Headers []Headers
TLS TLSConfig TLS TLSConfig
Middleware []middleware.Middleware
Startup []func() error
} }
// Address returns the host:port of c as a string. // Address returns the host:port of c as a string.
...@@ -63,38 +67,6 @@ func (c Config) Address() string { ...@@ -63,38 +67,6 @@ func (c Config) Address() string {
return c.Host + ":" + c.Port return c.Host + ":" + c.Port
} }
// Rewrite describes an internal location rewrite.
type Rewrite struct {
From string
To string
}
// Redirect describes an HTTP redirect.
type Redirect struct {
From string
To string
Code int
}
// Log represents the settings for a log.
type Log struct {
Enabled bool
OutputFile string
Format string
}
// Headers groups a slice of HTTP headers by a URL pattern.
type Headers struct {
Url string
Headers []Header
}
// Header represents a single HTTP header, simply a name and value.
type Header struct {
Name string
Value string
}
// TLSConfig describes how TLS should be configured and used, // TLSConfig describes how TLS should be configured and used,
// if at all. At least a certificate and key are required. // if at all. At least a certificate and key are required.
type TLSConfig struct { type TLSConfig struct {
...@@ -102,89 +74,3 @@ type TLSConfig struct { ...@@ -102,89 +74,3 @@ type TLSConfig struct {
Certificate string Certificate string
Key string Key string
} }
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": 300,
"301": 301,
"302": 302,
"303": 303,
"304": 304,
"305": 305,
"306": 306,
"307": 307,
"308": 308,
}
// httpErrors is a list of supported HTTP error codes.
var httpErrors = map[string]int{
"400": 400,
"401": 401,
"402": 402,
"403": 403,
"404": 404,
"405": 405,
"406": 406,
"407": 407,
"408": 408,
"409": 409,
"410": 410,
"411": 411,
"412": 412,
"413": 413,
"414": 414,
"415": 415,
"416": 416,
"417": 417,
"418": 418,
"419": 419,
"420": 420,
"422": 422,
"423": 423,
"424": 424,
"426": 426,
"428": 428,
"429": 429,
"431": 431,
"440": 440,
"444": 444,
"449": 449,
"450": 450,
"451": 451,
"494": 494,
"495": 495,
"496": 496,
"497": 497,
"498": 498,
"499": 499,
"500": 500,
"501": 501,
"502": 502,
"503": 503,
"504": 504,
"505": 505,
"506": 506,
"507": 507,
"508": 508,
"509": 509,
"510": 510,
"511": 511,
"520": 520,
"521": 521,
"522": 522,
"523": 523,
"524": 524,
"598": 598,
"599": 599,
}
const (
defaultHost = "localhost"
defaultPort = "8080"
defaultRoot = "."
)
const (
DefaultRequestsLog = "requests.log"
DefaultErrorsLog = "errors.log"
)
package config package config
import "os"
// dirFunc is a type of parsing function which processes // dirFunc is a type of parsing function which processes
// a particular directive and populates the config. // a particular directive and populates the config.
type dirFunc func(*parser) error type dirFunc func(*parser) error
...@@ -15,23 +17,23 @@ func init() { ...@@ -15,23 +17,23 @@ func init() {
// invokes a method that uses this map. // invokes a method that uses this map.
validDirectives = map[string]dirFunc{ validDirectives = map[string]dirFunc{
"root": func(p *parser) error { "root": func(p *parser) error {
if !p.lexer.NextArg() { if !p.nextArg() {
return p.argErr() return p.argErr()
} }
p.cfg.Root = p.tkn() p.cfg.Root = p.tkn()
return nil return nil
}, },
"import": func(p *parser) error { "import": func(p *parser) error {
if !p.lexer.NextArg() { if !p.nextArg() {
return p.argErr() return p.argErr()
} }
p2 := parser{} file, err := os.Open(p.tkn())
err := p2.lexer.Load(p.tkn())
if err != nil { if err != nil {
return p.err("Parse", err.Error()) return p.err("Parse", err.Error())
} }
defer p2.lexer.Close() defer file.Close()
p2 := newParser(file)
p2.cfg = p.cfg p2.cfg = p.cfg
err = p2.directives() err = p2.directives()
...@@ -42,210 +44,15 @@ func init() { ...@@ -42,210 +44,15 @@ func init() {
return nil return nil
}, },
"gzip": func(p *parser) error {
p.cfg.Gzip = true
return nil
},
"log": func(p *parser) error {
log := Log{Enabled: true}
// Get the type of log (requests, errors, etc.)
if !p.lexer.NextArg() {
return p.argErr()
}
logWhat := p.tkn()
// Set the log output file
if p.lexer.NextArg() {
log.OutputFile = p.tkn()
}
// Set the log output format
if p.lexer.NextArg() {
log.Format = p.tkn()
}
switch logWhat {
case "requests":
if log.OutputFile == "" || log.OutputFile == "_" {
log.OutputFile = DefaultRequestsLog
}
p.cfg.RequestLog = log
case "errors":
if log.OutputFile == "" || log.OutputFile == "_" {
log.OutputFile = DefaultErrorsLog
}
p.cfg.ErrorLog = log
default:
return p.err("Parse", "Unknown log '"+logWhat+"'")
}
return nil
},
"rewrite": func(p *parser) error {
var rw Rewrite
if !p.lexer.NextArg() {
return p.argErr()
}
rw.From = p.tkn()
if !p.lexer.NextArg() {
return p.argErr()
}
rw.To = p.tkn()
p.cfg.Rewrites = append(p.cfg.Rewrites, rw)
return nil
},
"redir": func(p *parser) error {
var redir Redirect
// From
if !p.lexer.NextArg() {
return p.argErr()
}
redir.From = p.tkn()
// To
if !p.lexer.NextArg() {
return p.argErr()
}
redir.To = p.tkn()
// Status Code
if !p.lexer.NextArg() {
return p.argErr()
}
if code, ok := httpRedirs[p.tkn()]; !ok {
return p.err("Parse", "Invalid redirect code '"+p.tkn()+"'")
} else {
redir.Code = code
}
p.cfg.Redirects = append(p.cfg.Redirects, redir)
return nil
},
"ext": func(p *parser) error {
if !p.lexer.NextArg() {
return p.argErr()
}
p.cfg.Extensions = append(p.cfg.Extensions, p.tkn())
for p.lexer.NextArg() {
p.cfg.Extensions = append(p.cfg.Extensions, p.tkn())
}
return nil
},
"error": func(p *parser) error {
if !p.lexer.NextArg() {
return p.argErr()
}
if code, ok := httpErrors[p.tkn()]; !ok {
return p.err("Syntax", "Invalid error code '"+p.tkn()+"'")
} else if val, exists := p.cfg.ErrorPages[code]; exists {
return p.err("Config", p.tkn()+" error page already configured to be '"+val+"'")
} else {
if !p.lexer.NextArg() {
return p.argErr()
}
p.cfg.ErrorPages[code] = p.tkn()
}
return nil
},
"header": func(p *parser) error {
var head Headers
var isNewPattern bool
if !p.lexer.NextArg() {
return p.argErr()
}
pattern := p.tkn()
// See if we already have a definition for this URL pattern...
for _, h := range p.cfg.Headers {
if h.Url == pattern {
head = h
break
}
}
// ...otherwise, this is a new pattern
if head.Url == "" {
head.Url = pattern
isNewPattern = true
}
processHeaderBlock := func() error {
err := p.openCurlyBrace()
if err != nil {
return err
}
for p.lexer.Next() {
if p.tkn() == "}" {
break
}
h := Header{Name: p.tkn()}
if p.lexer.NextArg() {
h.Value = p.tkn()
}
head.Headers = append(head.Headers, h)
}
err = p.closeCurlyBrace()
if err != nil {
return err
}
return nil
}
// A single header could be declared on the same line, or
// multiple headers can be grouped by URL pattern, so we have
// to look for both here.
if p.lexer.NextArg() {
if p.tkn() == "{" {
err := processHeaderBlock()
if err != nil {
return err
}
} else {
h := Header{Name: p.tkn()}
if p.lexer.NextArg() {
h.Value = p.tkn()
}
head.Headers = append(head.Headers, h)
}
} else {
// Okay, it might be an opening curly brace on the next line
if !p.lexer.Next() {
return p.eofErr()
}
err := processHeaderBlock()
if err != nil {
return err
}
}
if isNewPattern {
p.cfg.Headers = append(p.cfg.Headers, head)
} else {
for i := 0; i < len(p.cfg.Headers); i++ {
if p.cfg.Headers[i].Url == pattern {
p.cfg.Headers[i] = head
break
}
}
}
return nil
},
"tls": func(p *parser) error { "tls": func(p *parser) error {
tls := TLSConfig{Enabled: true} tls := TLSConfig{Enabled: true}
if !p.lexer.NextArg() { if !p.nextArg() {
return p.argErr() return p.argErr()
} }
tls.Certificate = p.tkn() tls.Certificate = p.tkn()
if !p.lexer.NextArg() { if !p.nextArg() {
return p.argErr() return p.argErr()
} }
tls.Key = p.tkn() tls.Key = p.tkn()
......
package config
import (
"errors"
"fmt"
"github.com/mholt/caddy/middleware"
)
// dispenser is a type that gets exposed to middleware
// generators so that they can parse tokens to configure
// their instance.
type dispenser struct {
parser *parser
iter int
tokens []token
err error
}
// newDispenser returns a new dispenser.
func newDispenser(p *parser) *dispenser {
d := new(dispenser)
d.iter = -1
d.parser = p
return d
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
// TODO: Have the other Next functions call this one...?
func (d *dispenser) Next() bool {
if d.iter >= len(d.tokens)-1 {
return false
} else {
d.iter++
return true
}
}
// NextArg loads the next token if it is on the same
// line. Returns true if a token was loaded; false
// otherwise. If false, all tokens on the line have
// been consumed.
func (d *dispenser) NextArg() bool {
if d.iter < 0 {
d.iter++
return true
}
if d.iter >= len(d.tokens) {
return false
}
if d.iter < len(d.tokens)-1 &&
d.tokens[d.iter].line == d.tokens[d.iter+1].line {
d.iter++
return true
}
return false
}
// TODO: Keep this method? It's like NextArg
// but only gets the next token if it's on the next line...
func (d *dispenser) NextLine() bool {
if d.iter < 0 {
d.iter++
return true
}
if d.iter >= len(d.tokens) {
return false
}
if d.iter < len(d.tokens)-1 &&
d.tokens[d.iter].line < d.tokens[d.iter+1].line {
d.iter++
return true
}
return false
}
// OpenCurlyBrace asserts that the current token is
// an opening curly brace "{". If it isn't, an error
// is produced and false is returned.
func (d *dispenser) OpenCurlyBrace() bool {
if d.Val() == "{" {
return true
} else {
d.Err("Parse", "Expected '{'")
return false
}
}
// CloseCurlyBrace asserts that the current token is
// a closing curly brace "}". If it isn't, an error
// is produced and false is returned.
func (d *dispenser) CloseCurlyBrace() bool {
if d.Val() == "}" {
return true
} else {
d.Err("Parse", "Expected '}'")
return false
}
}
// Val gets the text of the current token.
func (d *dispenser) Val() string {
if d.iter >= len(d.tokens) || d.iter < 0 {
return ""
} else {
return d.tokens[d.iter].text
}
}
// ArgErr generates an argument error, meaning that another
// argument was expected but not found. The error is saved
// within the dispenser, but this function returns nil for
// convenience.
func (d *dispenser) ArgErr() middleware.Middleware {
if d.Val() == "{" {
d.Err("Syntax", "Unexpected token '{', expecting argument for directive")
return nil
}
d.Err("Syntax", "Unexpected line break after '"+d.tokens[d.iter].text+"' (missing arguments?)")
return nil
}
// Err generates a custom error of type kind and with a message
// of msg. The kind should be capitalized. This function returns
// nil for convenience, but loads the error into the dispenser
// so it can be reported immediately.
func (d *dispenser) Err(kind, msg string) middleware.Middleware {
msg = fmt.Sprintf("%s:%d - %s error: %s", d.parser.filename, d.tokens[d.iter].line, kind, msg)
d.err = errors.New(msg)
return nil
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are fewer tokens available
// than string pointers, the remaining strings will not be changed.
func (d *dispenser) Args(targets ...*string) {
i := 0
for d.NextArg() {
*targets[i] = d.Val()
i++
}
}
// Startup registers a function to execute when the server starts.
func (d *dispenser) Startup(fn func() error) {
d.parser.cfg.Startup = append(d.parser.cfg.Startup, fn)
}
// Root returns the server root file path.
func (d *dispenser) Root() string {
if d.parser.cfg.Root == "" {
return "."
} else {
return d.parser.cfg.Root
}
}
// Host returns the hostname the server is bound to.
func (d *dispenser) Host() string {
return d.parser.cfg.Host
}
// Port returns the port that the server is listening on.
func (d *dispenser) Port() string {
return d.parser.cfg.Port
}
...@@ -3,58 +3,35 @@ package config ...@@ -3,58 +3,35 @@ package config
import ( import (
"bufio" "bufio"
"io" "io"
"os"
"unicode" "unicode"
) )
// Lexer is a utility which can get values, token by // lexer is a utility which can get values, token by
// token, from a config file. A token is a word, and tokens // token, from a reader. A token is a word, and tokens
// are separated by whitespace. A word can be enclosed in // are separated by whitespace. A word can be enclosed in
// quotes if it contains whitespace. // quotes if it contains whitespace.
type lexer struct { type lexer struct {
file *os.File
reader *bufio.Reader reader *bufio.Reader
token token token token
line int line int
} }
// Load opens a file and prepares to scan the file. // load prepares the lexer to scan a file for tokens.
func (l *lexer) Load(filename string) error { func (l *lexer) load(file io.Reader) error {
f, err := os.Open(filename) l.reader = bufio.NewReader(file)
if err != nil {
return err
}
l.reader = bufio.NewReader(f)
l.file = f
l.line = 1 l.line = 1
return nil return nil
} }
// Close closes the file. // next loads the next token into the lexer.
func (l *lexer) Close() { // A token is delimited by whitespace, unless
l.file.Close() // the token starts with a quotes character (")
} // in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// Next gets the next token from the input. The resulting token // The rest of the line is skipped if a "#"
// is in l.token if next returns true. If Next returns false, // character is read in. Returns true if a token
// there are no more tokens. // was loaded; false otherwise.
func (l *lexer) Next() bool { func (l *lexer) next() bool {
return l.next(true)
}
// NextArg works just like Next, but returns false if the next
// token is not on the same line as the one before. This method
// makes it easier to throw syntax errors when more values are
// expected on the same line.
func (l *lexer) NextArg() bool {
return l.next(false)
}
// next gets the next token according to newlineOK, which
// specifies whether it's OK if the next token is on another
// line. Returns true if there was a new token loaded, false
// otherwise.
func (l *lexer) next(newlineOK bool) bool {
var val []rune var val []rune
var comment, quoted, escaped bool var comment, quoted, escaped bool
...@@ -99,21 +76,15 @@ func (l *lexer) next(newlineOK bool) bool { ...@@ -99,21 +76,15 @@ func (l *lexer) next(newlineOK bool) bool {
} }
if unicode.IsSpace(ch) { if unicode.IsSpace(ch) {
if ch == '\r' {
continue
}
if ch == '\n' { if ch == '\n' {
l.line++ l.line++
comment = false comment = false
} }
if len(val) > 0 { if len(val) > 0 {
return makeToken() return makeToken()
} else if !newlineOK {
err := l.reader.UnreadRune()
if err != nil {
panic(err)
}
if ch == '\n' {
l.line--
}
return false
} }
continue continue
} }
...@@ -138,8 +109,7 @@ func (l *lexer) next(newlineOK bool) bool { ...@@ -138,8 +109,7 @@ func (l *lexer) next(newlineOK bool) bool {
} }
} }
// A token represents a single valuable/processable unit // token represents a single processable unit.
// in a config file.
type token struct { type token struct {
line int line int
text string text string
......
...@@ -3,34 +3,121 @@ package config ...@@ -3,34 +3,121 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/mholt/caddy/middleware"
) )
// parser is a type which can parse config files. // parser is a type which can parse config files.
type parser struct { type parser struct {
lexer lexer filename string // the name of the file that we're parsing
cfg Config 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]*dispenser // tokens to be parsed later by others (middleware generators)
unused bool // sometimes the token won't be immediately consumed
}
// newParser makes a new parser and prepares it for parsing, given
// the input to parse.
func newParser(file *os.File) *parser {
p := &parser{filename: file.Name()}
p.lexer.load(file)
return p
} }
// Parse parses the configuration file. It produces a slice of Config // Parse parses the configuration file. It produces a slice of Config
// structs which can be used to create and configure server instances. // structs which can be used to create and configure server instances.
func (p *parser) Parse() ([]Config, error) { func (p *parser) parse() ([]Config, error) {
var configs []Config var configs []Config
for p.lexer.Next() { for p.lexer.next() {
p.cfg = Config{ErrorPages: make(map[int]string)} err := p.parseOne()
err := p.parse()
if err != nil { if err != nil {
return configs, err return nil, err
} }
configs = append(configs, p.cfg) configs = append(configs, p.cfg)
} }
return configs, nil return configs, nil
} }
// nextArg loads the next token if it is on the same line.
// Returns true if a token was loaded; false otherwise.
func (p *parser) nextArg() bool {
if p.unused {
return false
}
line := p.line()
if p.next() {
if p.line() > line {
p.unused = true
return false
}
return true
}
return false
}
// next loads the next token and returns true if a token
// was loaded; false otherwise.
func (p *parser) next() bool {
if p.unused {
p.unused = false
return true
} else {
return p.lexer.next()
}
}
// parseOne parses the contents of a configuration
// 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
// block already.
func (p *parser) parseOne() error {
p.cfg = Config{}
p.other = make(map[string]*dispenser)
err := p.begin()
if err != nil {
return err
}
err = p.unwrap()
if err != nil {
return err
}
return nil
}
// 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.
// 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 middleware.Ordered() {
if disp, ok := p.other[directive]; ok {
if generator, ok := middleware.GetGenerator(directive); ok {
mid := generator(disp)
if mid == nil {
return disp.err
}
p.cfg.Middleware = append(p.cfg.Middleware, mid)
} else {
return errors.New("No middleware bound to directive '" + directive + "'")
}
}
}
return nil
}
// tkn is shorthand to get the text/value of the current token. // tkn is shorthand to get the text/value of the current token.
func (p *parser) tkn() string { func (p *parser) tkn() string {
return p.lexer.token.text return p.lexer.token.text
...@@ -58,10 +145,10 @@ func (p *parser) eofErr() error { ...@@ -58,10 +145,10 @@ func (p *parser) eofErr() error {
return p.err("Syntax", "Unexpected EOF") return p.err("Syntax", "Unexpected EOF")
} }
// err creates a "{{kind}} error: ..." with a custom message msg. The // err creates an error with a custom message msg: "{{kind}} error: {{msg}}". The
// file name and line number are included in the error message. // file name and line number are included in the error message.
func (p *parser) err(kind, msg string) error { func (p *parser) err(kind, msg string) error {
msg = fmt.Sprintf("%s error: %s:%d - %s", kind, p.lexer.file.Name(), p.line(), msg) msg = fmt.Sprintf("%s:%d - %s error: %s", p.filename, p.line(), kind, msg)
return errors.New(msg) return errors.New(msg)
} }
......
package config package config
import "github.com/mholt/caddy/middleware"
// This file contains the recursive-descent parsing // This file contains the recursive-descent parsing
// functions. // functions.
// parse is the top of the recursive-descent parsing. // begin is the top of the recursive-descent parsing.
// It parses at most 1 server configuration (an address // It parses at most one server configuration (an address
// and its directives). // and its directives).
func (p *parser) parse() error { func (p *parser) begin() error {
err := p.address() err := p.address()
if err != nil { if err != nil {
return err return err
...@@ -23,15 +25,23 @@ func (p *parser) parse() error { ...@@ -23,15 +25,23 @@ func (p *parser) parse() error {
// address expects that the current token is a host:port // address expects that the current token is a host:port
// combination. // combination.
func (p *parser) address() error { func (p *parser) address() error {
if p.tkn() == "}" || p.tkn() == "{" {
return p.err("Syntax", "'"+p.tkn()+"' is not a listening address or EOF")
}
p.cfg.Host, p.cfg.Port = parseAddress(p.tkn()) p.cfg.Host, p.cfg.Port = parseAddress(p.tkn())
p.lexer.Next()
return nil return nil
} }
// addressBlock leads into parsing directives. It // addressBlock leads into parsing directives, including
// handles directives enclosed by curly braces and // possible opening/closing curly braces around the block.
// It handles directives enclosed by curly braces and
// directives not enclosed by curly braces. // directives not enclosed by curly braces.
func (p *parser) addressBlock() error { func (p *parser) addressBlock() error {
if !p.next() {
// file consisted of only an address
return nil
}
err := p.openCurlyBrace() err := p.openCurlyBrace()
if err != nil { if err != nil {
// meh, single-server configs don't need curly braces // meh, single-server configs don't need curly braces
...@@ -51,7 +61,9 @@ func (p *parser) addressBlock() error { ...@@ -51,7 +61,9 @@ func (p *parser) addressBlock() error {
} }
// openCurlyBrace expects the current token to be an // openCurlyBrace expects the current token to be an
// opening curly brace. // opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// a opening curly brace.
func (p *parser) openCurlyBrace() error { func (p *parser) openCurlyBrace() error {
if p.tkn() != "{" { if p.tkn() != "{" {
return p.syntaxErr("{") return p.syntaxErr("{")
...@@ -60,6 +72,8 @@ func (p *parser) openCurlyBrace() error { ...@@ -60,6 +72,8 @@ func (p *parser) openCurlyBrace() error {
} }
// closeCurlyBrace expects the current token to be // closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
// a closing curly brace. // a closing curly brace.
func (p *parser) closeCurlyBrace() error { func (p *parser) closeCurlyBrace() error {
if p.tkn() != "}" { if p.tkn() != "}" {
...@@ -73,18 +87,67 @@ func (p *parser) closeCurlyBrace() error { ...@@ -73,18 +87,67 @@ func (p *parser) closeCurlyBrace() error {
// directive. It goes until EOF or closing curly // directive. It goes until EOF or closing curly
// brace. // brace.
func (p *parser) directives() error { func (p *parser) directives() error {
for p.lexer.Next() { for p.next() {
if p.tkn() == "}" { if p.tkn() == "}" {
// end of address scope
break break
} }
if fn, ok := validDirectives[p.tkn()]; !ok { if fn, ok := validDirectives[p.tkn()]; ok {
return p.syntaxErr("[directive]")
} else {
err := fn(p) err := fn(p)
if err != nil { if err != nil {
return err return err
} }
} else if middleware.Registered(p.tkn()) {
err := p.collectTokens()
if err != nil {
return err
}
} else {
return p.err("Syntax", "Unexpected token '"+p.tkn()+"', expecting a valid directive")
}
}
return nil
}
// collectTokens consumes tokens until the directive's scope
// closes (either end of line or end of curly brace block).
func (p *parser) collectTokens() error {
directive := p.tkn()
line := p.line()
nesting := 0
breakOk := false
disp := newDispenser(p)
// Re-use a duplicate directive's dispenser from before
// (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 {
disp = existing
}
// The directive is appended as a relevant token
disp.tokens = append(disp.tokens, p.lexer.token)
for p.next() {
if p.tkn() == "{" {
nesting++
} else if p.line() > line && nesting == 0 {
p.unused = true
breakOk = true
break
} else if p.tkn() == "}" && nesting > 0 {
nesting--
} else if p.tkn() == "}" && nesting == 0 {
return p.err("Syntax", "Unexpected '}' because no matching open curly brace '{'")
} }
disp.tokens = append(disp.tokens, p.lexer.token)
} }
if !breakOk || nesting > 0 {
return p.eofErr()
}
p.other[directive] = disp
return nil return nil
} }
...@@ -9,6 +9,12 @@ import ( ...@@ -9,6 +9,12 @@ import (
"github.com/mholt/caddy/server" "github.com/mholt/caddy/server"
) )
var conf string
func init() {
flag.StringVar(&conf, "conf", server.DefaultConfigFile, "the configuration file to use")
}
func main() { func main() {
var wg sync.WaitGroup var wg sync.WaitGroup
...@@ -40,9 +46,3 @@ func main() { ...@@ -40,9 +46,3 @@ func main() {
wg.Wait() wg.Wait()
} }
func init() {
flag.StringVar(&conf, "conf", server.DefaultConfigFile, "the configuration file to use")
}
var conf string
...@@ -10,11 +10,24 @@ import ( ...@@ -10,11 +10,24 @@ import (
// passed in as well as possible extensions to add, internally, // passed in as well as possible extensions to add, internally,
// to paths requested. The first path+ext that matches a resource // to paths requested. The first path+ext that matches a resource
// that exists will be used. // that exists will be used.
func Extensionless(root string, extensions []string) Middleware { func Extensionless(p parser) Middleware {
var extensions []string
var root = p.Root() // TODO: Big gotcha! Save this now before it goes away! We can't get this later during a request!
for p.Next() {
if !p.NextArg() {
return p.ArgErr()
}
extensions = append(extensions, p.Val())
for p.NextArg() {
extensions = append(extensions, p.Val())
}
}
resourceExists := func(path string) bool { resourceExists := func(path string) bool {
_, err := os.Stat(root + path) _, err := os.Stat(root + path)
// technically we should use os.IsNotExist(err) // technically we should use os.IsNotExist(err)
// but we don't handle any other error types anyway // but we don't handle any other kinds of errors anyway
return err == nil return err == nil
} }
......
...@@ -7,20 +7,19 @@ import ( ...@@ -7,20 +7,19 @@ import (
"strings" "strings"
) )
// Adapted from https://gist.github.com/the42/1956518 func Gzip(p parser) Middleware {
return func(next http.HandlerFunc) http.HandlerFunc {
// Gzip is middleware that gzip-compresses the response. return func(w http.ResponseWriter, r *http.Request) {
func Gzip(next http.HandlerFunc) http.HandlerFunc { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
return func(w http.ResponseWriter, r *http.Request) { next(w, r)
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { return
next(w, r) }
return w.Header().Set("Content-Encoding", "gzip")
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
gz := gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}
next(gz, r)
} }
w.Header().Set("Content-Encoding", "gzip")
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
gz := gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}
next(gz, r)
} }
} }
...@@ -36,5 +35,6 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) { ...@@ -36,5 +35,6 @@ func (w gzipResponseWriter) Write(b []byte) (int, error) {
if w.Header().Get("Content-Type") == "" { if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", http.DetectContentType(b)) w.Header().Set("Content-Type", http.DetectContentType(b))
} }
return w.Writer.Write(b) n, err := w.Writer.Write(b)
return n, err
} }
package middleware package middleware
import ( import "net/http"
"net/http"
"strings"
"github.com/mholt/caddy/config"
)
// Headers is middleware that adds headers to the responses // Headers is middleware that adds headers to the responses
// for requests matching a certain path. // for requests matching a certain path.
func Headers(headers []config.Headers) Middleware { func Headers(p parser) Middleware {
type (
// Header represents a single HTTP header, simply a name and value.
header struct {
Name string
Value string
}
// Headers groups a slice of HTTP headers by a URL pattern.
headers struct {
Url string
Headers []header
}
)
var rules []headers
for p.Next() {
var head headers
var isNewPattern bool
if !p.NextArg() {
return p.ArgErr()
}
pattern := p.Val()
// See if we already have a definition for this URL pattern...
for _, h := range rules {
if h.Url == pattern {
head = h
break
}
}
// ...otherwise, this is a new pattern
if head.Url == "" {
head.Url = pattern
isNewPattern = true
}
processHeaderBlock := func() bool {
if !p.OpenCurlyBrace() {
return false
}
for p.Next() {
if p.Val() == "}" {
break
}
h := header{Name: p.Val()}
if p.NextArg() {
h.Value = p.Val()
}
head.Headers = append(head.Headers, h)
}
if !p.CloseCurlyBrace() {
return false
}
return true
}
// A single header could be declared on the same line, or
// multiple headers can be grouped by URL pattern, so we have
// to look for both here.
if p.NextArg() {
if p.Val() == "{" {
if !processHeaderBlock() {
return nil
}
} else {
h := header{Name: p.Val()}
if p.NextArg() {
h.Value = p.Val()
}
head.Headers = append(head.Headers, h)
}
} else {
// Okay, it might be an opening curly brace on the next line
if !p.Next() {
return p.Err("Parse", "Unexpected EOF")
}
if !processHeaderBlock() {
return nil
}
}
if isNewPattern {
rules = append(rules, head)
} else {
for i := 0; i < len(rules); i++ {
if rules[i].Url == pattern {
rules[i] = head
break
}
}
}
}
return func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
for _, rule := range headers { for _, rule := range rules {
if pathsMatch(r.URL.Path, rule.Url) { if Path(r.URL.Path).Matches(rule.Url) {
for _, header := range rule.Headers { for _, header := range rule.Headers {
w.Header().Set(header.Name, header.Value) w.Header().Set(header.Name, header.Value)
} }
...@@ -23,13 +113,3 @@ func Headers(headers []config.Headers) Middleware { ...@@ -23,13 +113,3 @@ func Headers(headers []config.Headers) Middleware {
} }
} }
} }
// Returns whether or not p1 and p2 are matching
// paths. This can be defined a number of ways
// and it is not for sure yet how to match URL/path
// strings. It may be a prefix match or a full
// string match, it may strip trailing slashes.
// Until the software hits 1.0, this will be in flux.
func pathsMatch(p1, p2 string) bool {
return strings.HasPrefix(p1, p2)
}
...@@ -3,40 +3,65 @@ package middleware ...@@ -3,40 +3,65 @@ package middleware
import ( import (
"log" "log"
"net/http" "net/http"
"os"
) )
func RequestLog(logger *log.Logger, format string) Middleware { func RequestLog(p parser) Middleware {
if format == "" { var logWhat, outputFile, format string
format = defaultReqLogFormat var logger *log.Logger
}
return func(next http.HandlerFunc) http.HandlerFunc { for p.Next() {
return func(w http.ResponseWriter, r *http.Request) { p.Args(&logWhat, &outputFile, &format)
sw := newResponseRecorder(w)
next(sw, r) if logWhat == "" {
rep := newReplacer(r, sw) return p.ArgErr()
logger.Println(rep.replace(format)) }
if outputFile == "" {
outputFile = defaultLogFilename
}
switch format {
case "":
format = defaultReqLogFormat
case "{common}":
format = commonLogFormat
case "{combined}":
format = combinedLogFormat
} }
} }
}
// TODO. // Open the log file for writing when the server starts
func ErrorLog(logger *log.Logger, format string) Middleware { p.Startup(func() error {
if format == "" { var err error
format = defaultErrLogFormat var file *os.File
}
if outputFile == "stdout" {
file = os.Stdout
} else if outputFile == "stderr" {
file = os.Stderr
} else {
file, err = os.OpenFile(outputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
}
logger = log.New(file, "", 0)
return nil
})
return func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
sw := newResponseRecorder(w) sw := newResponseRecorder(w)
next(sw, r) next(sw, r)
// This is still TODO -- we need to define what constitutes an error to be logged rep := newReplacer(r, sw)
//logger.Println("TODO") logger.Println(rep.replace(format))
} }
} }
} }
const ( const (
defaultLogFilename = "access.log"
commonLogFormat = `{remote} ` + emptyStringReplacer + ` [{time}] "{method} {uri} {proto}" {status} {size}` commonLogFormat = `{remote} ` + emptyStringReplacer + ` [{time}] "{method} {uri} {proto}" {status} {size}`
combinedLogFormat = commonLogFormat + ` "{>Referer}" "{>User-Agent}"` combinedLogFormat = commonLogFormat + ` "{>Referer}" "{>User-Agent}"`
defaultReqLogFormat = commonLogFormat defaultReqLogFormat = commonLogFormat
defaultErrLogFormat = "[TODO]"
) )
...@@ -2,10 +2,96 @@ ...@@ -2,10 +2,96 @@
// the servers to use, according to their configuration. // the servers to use, according to their configuration.
package middleware package middleware
import "net/http" import (
"net/http"
"strings"
)
// Middleware is a type of function that generates a new // This init function registers middleware. Register middleware
// layer of middleware. It is imperative that the HandlerFunc // in the order they should be executed during a request.
// being passed in is executed by the middleware, otherwise // Middlewares execute in an order like A-B-C-C-B-A.
// part of the stack will not be called. func init() {
type Middleware func(http.HandlerFunc) http.HandlerFunc register("gzip", Gzip)
register("header", Headers)
register("log", RequestLog)
register("rewrite", Rewrite)
register("redir", Redirect)
register("ext", Extensionless)
}
type (
// Generator represents the outer layer of a middleware that
// parses tokens to configure the middleware instance.
Generator func(parser) Middleware
// Middleware is the middle layer which represents the traditional
// idea of middleware: it is passed the next HandlerFunc in the chain
// and returns the inner layer, which is the actual HandlerFunc.
Middleware func(http.HandlerFunc) http.HandlerFunc
// parser is the type which middleware generators use to access
// tokens and other information they need to configure the instance.
parser interface {
Next() bool
NextArg() bool
NextLine() bool
Val() string
OpenCurlyBrace() bool
CloseCurlyBrace() bool
ArgErr() Middleware
Err(string, string) Middleware
Args(...*string)
Startup(func() error)
Root() string
Host() string
Port() string
}
)
var (
// registry stores the registered middleware:
// both the order and the directives to which they
// are bound.
registry = struct {
directiveMap map[string]Generator
order []string
}{
directiveMap: make(map[string]Generator),
}
)
// GetGenerator gets the generator function (outer layer)
// of a middleware, according to the directive passed in.
func GetGenerator(directive string) (Generator, bool) {
rm, ok := registry.directiveMap[directive]
return rm, ok
}
// register binds a middleware generator (outer function)
// to a directive. Upon each request, middleware will be
// executed in the order they are registered.
func register(directive string, generator Generator) {
registry.directiveMap[directive] = generator
registry.order = append(registry.order, directive)
}
// Ordered returns the ordered list of registered directives.
func Ordered() []string {
return registry.order
}
// Registered returns whether or not a directive is registered.
func Registered(directive string) bool {
_, ok := GetGenerator(directive)
return ok
}
// Path represents a URI path, maybe with pattern characters.
type Path string
// Path matching will probably not always be a direct
// comparison; this method assures that paths can be
// easily matched.
func (p Path) Matches(other string) bool {
return strings.HasPrefix(string(p), other)
}
package middleware package middleware
import ( import "net/http"
"net/http"
"github.com/mholt/caddy/config"
)
// Redirect is middleware for redirecting certain requests // Redirect is middleware for redirecting certain requests
// to other locations. // to other locations.
func Redirect(redirs []config.Redirect) Middleware { func Redirect(p parser) Middleware {
// Redirect describes an HTTP redirect rule.
type redirect struct {
From string
To string
Code int
}
var redirects []redirect
for p.Next() {
var rule redirect
// From
if !p.NextArg() {
return p.ArgErr()
}
rule.From = p.Val()
// To
if !p.NextArg() {
return p.ArgErr()
}
rule.To = p.Val()
// Status Code
if !p.NextArg() {
return p.ArgErr()
}
if code, ok := httpRedirs[p.Val()]; !ok {
return p.Err("Parse", "Invalid redirect code '"+p.Val()+"'")
} else {
rule.Code = code
}
redirects = append(redirects, rule)
}
return func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
for _, rule := range redirs { for _, rule := range redirects {
if r.URL.Path == rule.From { if r.URL.Path == rule.From {
http.Redirect(w, r, rule.To, rule.Code) http.Redirect(w, r, rule.To, rule.Code)
break break
...@@ -21,3 +56,16 @@ func Redirect(redirs []config.Redirect) Middleware { ...@@ -21,3 +56,16 @@ func Redirect(redirs []config.Redirect) Middleware {
} }
} }
} }
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": 300,
"301": 301,
"302": 302,
"303": 303,
"304": 304,
"305": 305,
"306": 306,
"307": 307,
"308": 308,
}
package middleware package middleware
import ( import "net/http"
"net/http"
"github.com/mholt/caddy/config"
)
// Rewrite is middleware for rewriting requests internally to // Rewrite is middleware for rewriting requests internally to
// a different path. // a different path.
func Rewrite(rewrites []config.Rewrite) Middleware { func Rewrite(p parser) Middleware {
// Rewrite describes an internal location rewrite rule.
type rewrite struct {
From string
To string
}
var rewrites []rewrite
for p.Next() {
var rule rewrite
if !p.NextArg() {
return p.ArgErr()
}
rule.From = p.Val()
if !p.NextArg() {
return p.ArgErr()
}
rule.To = p.Val()
rewrites = append(rewrites, rule)
}
return func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
for _, rule := range rewrites { for _, rule := range rewrites {
......
...@@ -4,13 +4,15 @@ import ( ...@@ -4,13 +4,15 @@ import (
"errors" "errors"
"log" "log"
"net/http" "net/http"
"os"
"github.com/mholt/caddy/config" "github.com/mholt/caddy/config"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
) )
// servers maintains a registry of running servers. // The default configuration file to load if none is specified
const DefaultConfigFile = "Caddyfile"
// servers maintains a registry of running servers, keyed by address.
var servers = make(map[string]*Server) var servers = make(map[string]*Server)
// Server represents an instance of a server, which serves // Server represents an instance of a server, which serves
...@@ -46,7 +48,7 @@ func New(conf config.Config) (*Server, error) { ...@@ -46,7 +48,7 @@ func New(conf config.Config) (*Server, error) {
// Serve starts the server. It blocks until the server quits. // Serve starts the server. It blocks until the server quits.
func (s *Server) Serve() error { func (s *Server) Serve() error {
err := s.configureStack() err := s.buildStack()
if err != nil { if err != nil {
return err return err
} }
...@@ -73,73 +75,20 @@ func (s *Server) Log(v ...interface{}) { ...@@ -73,73 +75,20 @@ func (s *Server) Log(v ...interface{}) {
} }
} }
// configureStack builds the server's middleware stack based // buildStack builds the server's middleware stack based
// on its config. This method should be called last before // on its config. This method should be called last before
// ListenAndServe begins. // ListenAndServe begins.
func (s *Server) configureStack() error { func (s *Server) buildStack() error {
var mid []middleware.Middleware s.fileServer = http.FileServer(http.Dir(s.config.Root))
var err error
conf := s.config
// FileServer is the main application layer
s.fileServer = http.FileServer(http.Dir(conf.Root))
// push prepends each middleware to the stack so the
// compilation can iterate them in a natural, increasing order
push := func(m middleware.Middleware) {
mid = append(mid, nil)
copy(mid[1:], mid[0:])
mid[0] = m
}
// BEGIN ADDING MIDDLEWARE
// Middleware will be executed in the order they're added.
if conf.RequestLog.Enabled {
if conf.RequestLog.Enabled {
s.reqlog, err = enableLogging(conf.RequestLog)
if err != nil {
return err
}
}
push(middleware.RequestLog(s.reqlog, conf.RequestLog.Format))
}
if conf.ErrorLog.Enabled { for _, start := range s.config.Startup {
if conf.ErrorLog.Enabled { err := start()
s.errlog, err = enableLogging(conf.ErrorLog) if err != nil {
if err != nil { return err
return err
}
} }
push(middleware.ErrorLog(s.errlog, conf.ErrorLog.Format))
}
if len(conf.Rewrites) > 0 {
push(middleware.Rewrite(conf.Rewrites))
}
if len(conf.Redirects) > 0 {
push(middleware.Redirect(conf.Redirects))
}
if len(conf.Extensions) > 0 {
push(middleware.Extensionless(conf.Root, conf.Extensions))
} }
if len(conf.Headers) > 0 { s.compile(s.config.Middleware)
push(middleware.Headers(conf.Headers))
}
if conf.Gzip {
push(middleware.Gzip)
}
// END ADDING MIDDLEWARE
// Compiling the middleware unwraps each HandlerFunc,
// fully configured, ready to serve every request.
s.compile(mid)
return nil return nil
} }
...@@ -152,27 +101,3 @@ func (s *Server) compile(layers []middleware.Middleware) { ...@@ -152,27 +101,3 @@ func (s *Server) compile(layers []middleware.Middleware) {
s.stack = layer(s.stack) s.stack = layer(s.stack)
} }
} }
// enableLogging opens a log file and keeps it open for the lifetime
// of the server. In fact, the log file is never closed as long as
// the program is running, since the server will be running for
// that long. If that ever changes, the log file should be closed.
func enableLogging(l config.Log) (*log.Logger, error) {
var file *os.File
var err error
if l.OutputFile == "stdout" {
file = os.Stdout
} else if l.OutputFile == "stderr" {
file = os.Stderr
} else {
file, err = os.OpenFile(l.OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
}
return log.New(file, "", 0), nil
}
const DefaultConfigFile = "Caddyfile"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment