From 24fc2ae59ed94720625cb29a71e4960dc48462e7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 18 Jan 2015 23:11:21 -0700 Subject: [PATCH] Major refactoring; more modular middleware --- config/config.go | 152 ++++---------------------- config/directives.go | 211 ++---------------------------------- config/dispenser.go | 169 +++++++++++++++++++++++++++++ config/lexer.go | 66 +++-------- config/parser.go | 109 +++++++++++++++++-- config/parsing.go | 85 +++++++++++++-- main.go | 12 +- middleware/extensionless.go | 17 ++- middleware/gzip.go | 28 ++--- middleware/headers.go | 118 ++++++++++++++++---- middleware/log.go | 63 +++++++---- middleware/middleware.go | 98 ++++++++++++++++- middleware/redirect.go | 62 +++++++++-- middleware/rewrite.go | 33 +++++- server/server.go | 101 +++-------------- 15 files changed, 752 insertions(+), 572 deletions(-) create mode 100644 config/dispenser.go diff --git a/config/config.go b/config/config.go index cf5214d92..5fb31204a 100644 --- a/config/config.go +++ b/config/config.go @@ -2,20 +2,30 @@ // launching specially-configured server instances. package config -import "os" +import ( + "os" + + "github.com/mholt/caddy/middleware" +) + +const ( + defaultHost = "localhost" + defaultPort = "8080" + defaultRoot = "." +) // Load loads a configuration file, parses it, // and returns a slice of Config structs which // can be used to create and configure server // instances. func Load(filename string) ([]Config, error) { - p := parser{} - err := p.lexer.Load(filename) + file, err := os.Open(filename) if err != nil { return nil, err } - defer p.lexer.Close() - return p.Parse() + defer file.Close() + p := newParser(file) + return p.parse() } // IsNotFound returns whether or not the error is @@ -41,21 +51,15 @@ func Default() []Config { } // config represents a server configuration. It -// is populated by parsing a config file. (Use -// the Load function.) +// is populated by parsing a config file (via the +// Load function). type Config struct { Host string Port 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 + Middleware []middleware.Middleware + Startup []func() error } // Address returns the host:port of c as a string. @@ -63,38 +67,6 @@ func (c Config) Address() string { 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, // if at all. At least a certificate and key are required. type TLSConfig struct { @@ -102,89 +74,3 @@ type TLSConfig struct { Certificate 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" -) diff --git a/config/directives.go b/config/directives.go index 01ddb3b19..16d0e897f 100644 --- a/config/directives.go +++ b/config/directives.go @@ -1,5 +1,7 @@ package config +import "os" + // dirFunc is a type of parsing function which processes // a particular directive and populates the config. type dirFunc func(*parser) error @@ -15,23 +17,23 @@ func init() { // invokes a method that uses this map. validDirectives = map[string]dirFunc{ "root": func(p *parser) error { - if !p.lexer.NextArg() { + if !p.nextArg() { return p.argErr() } p.cfg.Root = p.tkn() return nil }, "import": func(p *parser) error { - if !p.lexer.NextArg() { + if !p.nextArg() { return p.argErr() } - p2 := parser{} - err := p2.lexer.Load(p.tkn()) + file, err := os.Open(p.tkn()) if err != nil { return p.err("Parse", err.Error()) } - defer p2.lexer.Close() + defer file.Close() + p2 := newParser(file) p2.cfg = p.cfg err = p2.directives() @@ -42,210 +44,15 @@ func init() { 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 := TLSConfig{Enabled: true} - if !p.lexer.NextArg() { + if !p.nextArg() { return p.argErr() } tls.Certificate = p.tkn() - if !p.lexer.NextArg() { + if !p.nextArg() { return p.argErr() } tls.Key = p.tkn() diff --git a/config/dispenser.go b/config/dispenser.go new file mode 100644 index 000000000..95b83f22d --- /dev/null +++ b/config/dispenser.go @@ -0,0 +1,169 @@ +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 +} diff --git a/config/lexer.go b/config/lexer.go index b504de811..564218983 100644 --- a/config/lexer.go +++ b/config/lexer.go @@ -3,58 +3,35 @@ package config import ( "bufio" "io" - "os" "unicode" ) -// Lexer is a utility which can get values, token by -// token, from a config file. A token is a word, and tokens +// lexer is a utility which can get values, token by +// token, from a reader. A token is a word, and tokens // are separated by whitespace. A word can be enclosed in // quotes if it contains whitespace. type lexer struct { - file *os.File reader *bufio.Reader token token line int } -// Load opens a file and prepares to scan the file. -func (l *lexer) Load(filename string) error { - f, err := os.Open(filename) - if err != nil { - return err - } - l.reader = bufio.NewReader(f) - l.file = f +// load prepares the lexer to scan a file for tokens. +func (l *lexer) load(file io.Reader) error { + l.reader = bufio.NewReader(file) l.line = 1 return nil } -// Close closes the file. -func (l *lexer) Close() { - l.file.Close() -} - -// Next gets the next token from the input. The resulting token -// is in l.token if next returns true. If Next returns false, -// there are no more tokens. -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 { +// next loads the next token into the lexer. +// A token is delimited by whitespace, unless +// the token starts with a quotes character (") +// in which case the token goes until the closing +// quotes (the enclosing quotes are not included). +// The rest of the line is skipped if a "#" +// character is read in. Returns true if a token +// was loaded; false otherwise. +func (l *lexer) next() bool { var val []rune var comment, quoted, escaped bool @@ -99,21 +76,15 @@ func (l *lexer) next(newlineOK bool) bool { } if unicode.IsSpace(ch) { + if ch == '\r' { + continue + } if ch == '\n' { l.line++ comment = false } if len(val) > 0 { return makeToken() - } else if !newlineOK { - err := l.reader.UnreadRune() - if err != nil { - panic(err) - } - if ch == '\n' { - l.line-- - } - return false } continue } @@ -138,8 +109,7 @@ func (l *lexer) next(newlineOK bool) bool { } } -// A token represents a single valuable/processable unit -// in a config file. +// token represents a single processable unit. type token struct { line int text string diff --git a/config/parser.go b/config/parser.go index 10d1e5f92..484fa9d3e 100644 --- a/config/parser.go +++ b/config/parser.go @@ -3,34 +3,121 @@ package config import ( "errors" "fmt" + "os" "strings" + + "github.com/mholt/caddy/middleware" ) // parser is a type which can parse config files. type parser struct { - lexer lexer - cfg Config + 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]*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 // 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 - for p.lexer.Next() { - p.cfg = Config{ErrorPages: make(map[int]string)} - - err := p.parse() + for p.lexer.next() { + err := p.parseOne() if err != nil { - return configs, err + return nil, err } - configs = append(configs, p.cfg) } 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. func (p *parser) tkn() string { return p.lexer.token.text @@ -58,10 +145,10 @@ func (p *parser) eofErr() error { 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. 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) } diff --git a/config/parsing.go b/config/parsing.go index 418311574..9c12d9bc7 100644 --- a/config/parsing.go +++ b/config/parsing.go @@ -1,12 +1,14 @@ package config +import "github.com/mholt/caddy/middleware" + // This file contains the recursive-descent parsing // functions. -// parse is the top of the recursive-descent parsing. -// It parses at most 1 server configuration (an address +// begin is the top of the recursive-descent parsing. +// It parses at most one server configuration (an address // and its directives). -func (p *parser) parse() error { +func (p *parser) begin() error { err := p.address() if err != nil { return err @@ -23,15 +25,23 @@ func (p *parser) parse() error { // address expects that the current token is a host:port // combination. 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.lexer.Next() return nil } -// addressBlock leads into parsing directives. It -// handles directives enclosed by curly braces and +// addressBlock leads into parsing directives, including +// possible opening/closing curly braces around the block. +// It handles directives enclosed by curly braces and // directives not enclosed by curly braces. func (p *parser) addressBlock() error { + if !p.next() { + // file consisted of only an address + return nil + } + err := p.openCurlyBrace() if err != nil { // meh, single-server configs don't need curly braces @@ -51,7 +61,9 @@ func (p *parser) addressBlock() error { } // 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 { if p.tkn() != "{" { return p.syntaxErr("{") @@ -60,6 +72,8 @@ func (p *parser) openCurlyBrace() error { } // 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. func (p *parser) closeCurlyBrace() error { if p.tkn() != "}" { @@ -73,18 +87,67 @@ func (p *parser) closeCurlyBrace() error { // directive. It goes until EOF or closing curly // brace. func (p *parser) directives() error { - for p.lexer.Next() { + for p.next() { if p.tkn() == "}" { + // end of address scope break } - if fn, ok := validDirectives[p.tkn()]; !ok { - return p.syntaxErr("[directive]") - } else { + if fn, ok := validDirectives[p.tkn()]; ok { err := fn(p) if err != nil { 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 +} diff --git a/main.go b/main.go index e81a9cd0d..ff8c8d126 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,12 @@ import ( "github.com/mholt/caddy/server" ) +var conf string + +func init() { + flag.StringVar(&conf, "conf", server.DefaultConfigFile, "the configuration file to use") +} + func main() { var wg sync.WaitGroup @@ -40,9 +46,3 @@ func main() { wg.Wait() } - -func init() { - flag.StringVar(&conf, "conf", server.DefaultConfigFile, "the configuration file to use") -} - -var conf string diff --git a/middleware/extensionless.go b/middleware/extensionless.go index fb19aefe3..6562e2596 100644 --- a/middleware/extensionless.go +++ b/middleware/extensionless.go @@ -10,11 +10,24 @@ import ( // passed in as well as possible extensions to add, internally, // to paths requested. The first path+ext that matches a resource // 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 { _, err := os.Stat(root + path) // 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 } diff --git a/middleware/gzip.go b/middleware/gzip.go index ec01c8e3d..397e5ce06 100644 --- a/middleware/gzip.go +++ b/middleware/gzip.go @@ -7,20 +7,19 @@ import ( "strings" ) -// Adapted from https://gist.github.com/the42/1956518 - -// Gzip is middleware that gzip-compresses the response. -func Gzip(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - next(w, r) - return +func Gzip(p parser) Middleware { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + 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) { if w.Header().Get("Content-Type") == "" { w.Header().Set("Content-Type", http.DetectContentType(b)) } - return w.Writer.Write(b) + n, err := w.Writer.Write(b) + return n, err } diff --git a/middleware/headers.go b/middleware/headers.go index f47953c4b..50891c2c7 100644 --- a/middleware/headers.go +++ b/middleware/headers.go @@ -1,19 +1,109 @@ package middleware -import ( - "net/http" - "strings" - - "github.com/mholt/caddy/config" -) +import "net/http" // Headers is middleware that adds headers to the responses // 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(w http.ResponseWriter, r *http.Request) { - for _, rule := range headers { - if pathsMatch(r.URL.Path, rule.Url) { + for _, rule := range rules { + if Path(r.URL.Path).Matches(rule.Url) { for _, header := range rule.Headers { w.Header().Set(header.Name, header.Value) } @@ -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) -} diff --git a/middleware/log.go b/middleware/log.go index 5b2022871..e8ce81dd9 100644 --- a/middleware/log.go +++ b/middleware/log.go @@ -3,12 +3,52 @@ package middleware import ( "log" "net/http" + "os" ) -func RequestLog(logger *log.Logger, format string) Middleware { - if format == "" { - format = defaultReqLogFormat +func RequestLog(p parser) Middleware { + var logWhat, outputFile, format string + var logger *log.Logger + + for p.Next() { + p.Args(&logWhat, &outputFile, &format) + + if logWhat == "" { + return p.ArgErr() + } + if outputFile == "" { + outputFile = defaultLogFilename + } + switch format { + case "": + format = defaultReqLogFormat + case "{common}": + format = commonLogFormat + case "{combined}": + format = combinedLogFormat + } } + + // Open the log file for writing when the server starts + p.Startup(func() error { + var err error + 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(w http.ResponseWriter, r *http.Request) { sw := newResponseRecorder(w) @@ -19,24 +59,9 @@ func RequestLog(logger *log.Logger, format string) Middleware { } } -// TODO. -func ErrorLog(logger *log.Logger, format string) Middleware { - if format == "" { - format = defaultErrLogFormat - } - return func(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - sw := newResponseRecorder(w) - next(sw, r) - // This is still TODO -- we need to define what constitutes an error to be logged - //logger.Println("TODO") - } - } -} - const ( + defaultLogFilename = "access.log" commonLogFormat = `{remote} ` + emptyStringReplacer + ` [{time}] "{method} {uri} {proto}" {status} {size}` combinedLogFormat = commonLogFormat + ` "{>Referer}" "{>User-Agent}"` defaultReqLogFormat = commonLogFormat - defaultErrLogFormat = "[TODO]" ) diff --git a/middleware/middleware.go b/middleware/middleware.go index b29fd1e7b..521cbbd84 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -2,10 +2,96 @@ // the servers to use, according to their configuration. package middleware -import "net/http" +import ( + "net/http" + "strings" +) -// Middleware is a type of function that generates a new -// layer of middleware. It is imperative that the HandlerFunc -// being passed in is executed by the middleware, otherwise -// part of the stack will not be called. -type Middleware func(http.HandlerFunc) http.HandlerFunc +// This init function registers middleware. Register middleware +// in the order they should be executed during a request. +// Middlewares execute in an order like A-B-C-C-B-A. +func init() { + 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) +} diff --git a/middleware/redirect.go b/middleware/redirect.go index 95b02609d..0a245160c 100644 --- a/middleware/redirect.go +++ b/middleware/redirect.go @@ -1,17 +1,52 @@ package middleware -import ( - "net/http" - - "github.com/mholt/caddy/config" -) +import "net/http" // Redirect is middleware for redirecting certain requests // 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(w http.ResponseWriter, r *http.Request) { - for _, rule := range redirs { + for _, rule := range redirects { if r.URL.Path == rule.From { http.Redirect(w, r, rule.To, rule.Code) break @@ -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, +} diff --git a/middleware/rewrite.go b/middleware/rewrite.go index bffd9c20d..3c180861d 100644 --- a/middleware/rewrite.go +++ b/middleware/rewrite.go @@ -1,14 +1,35 @@ package middleware -import ( - "net/http" - - "github.com/mholt/caddy/config" -) +import "net/http" // Rewrite is middleware for rewriting requests internally to // 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(w http.ResponseWriter, r *http.Request) { for _, rule := range rewrites { diff --git a/server/server.go b/server/server.go index ed98e87c3..0776b521d 100644 --- a/server/server.go +++ b/server/server.go @@ -4,13 +4,15 @@ import ( "errors" "log" "net/http" - "os" "github.com/mholt/caddy/config" "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) // Server represents an instance of a server, which serves @@ -46,7 +48,7 @@ func New(conf config.Config) (*Server, error) { // Serve starts the server. It blocks until the server quits. func (s *Server) Serve() error { - err := s.configureStack() + err := s.buildStack() if err != nil { return err } @@ -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 // ListenAndServe begins. -func (s *Server) configureStack() error { - var mid []middleware.Middleware - var err error - conf := s.config +func (s *Server) buildStack() error { + s.fileServer = http.FileServer(http.Dir(s.config.Root)) - // 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 - } + for _, start := range s.config.Startup { + err := start() + if err != nil { + return err } - push(middleware.RequestLog(s.reqlog, conf.RequestLog.Format)) } - if conf.ErrorLog.Enabled { - if conf.ErrorLog.Enabled { - s.errlog, err = enableLogging(conf.ErrorLog) - if err != nil { - 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 { - 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) + s.compile(s.config.Middleware) return nil } @@ -152,27 +101,3 @@ func (s *Server) compile(layers []middleware.Middleware) { 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"