From 6029973bdc9314ae32e59a91bc49199c5b865365 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 4 May 2015 11:04:17 -0600 Subject: [PATCH] Major refactoring of middleware and parser in progress --- config/directives.go | 4 +- config/parse/dispenser.go | 213 +++++++++++++++++++++++ config/parse/lexer.go | 114 ++++++++++++ config/parse/parse.go | 27 +++ config/parse/parsing.go | 300 ++++++++++++++++++++++++++++++++ config/setup/basicauth.go | 53 ++++++ config/setup/controller.go | 11 ++ config/setup/errors.go | 103 +++++++++++ config/setup/ext.go | 54 ++++++ config/setup/fastcgi.go | 105 +++++++++++ config/setup/gzip.go | 13 ++ config/setup/headers.go | 84 +++++++++ config/setup/log.go | 90 ++++++++++ config/setup/proxy.go | 145 +++++++++++++++ config/setup/redir.go | 77 ++++++++ config/setup/rewrite.go | 40 +++++ config/setup/root.go | 31 ++++ config/setup/startupshutdown.go | 43 +++++ config/setup/tls.go | 21 +++ middleware/fastcgi/fastcgi.go | 97 ----------- middleware/proxy/proxy.go | 20 +-- middleware/proxy/upstream.go | 138 +-------------- server/config.go | 50 ++++++ 23 files changed, 1588 insertions(+), 245 deletions(-) create mode 100644 config/parse/dispenser.go create mode 100644 config/parse/lexer.go create mode 100644 config/parse/parse.go create mode 100644 config/parse/parsing.go create mode 100644 config/setup/basicauth.go create mode 100644 config/setup/controller.go create mode 100644 config/setup/errors.go create mode 100644 config/setup/ext.go create mode 100644 config/setup/fastcgi.go create mode 100644 config/setup/gzip.go create mode 100644 config/setup/headers.go create mode 100644 config/setup/log.go create mode 100644 config/setup/proxy.go create mode 100644 config/setup/redir.go create mode 100644 config/setup/rewrite.go create mode 100644 config/setup/root.go create mode 100644 config/setup/startupshutdown.go create mode 100644 config/setup/tls.go create mode 100644 server/config.go diff --git a/config/directives.go b/config/directives.go index 76f29af73..f3d297fd5 100644 --- a/config/directives.go +++ b/config/directives.go @@ -29,8 +29,8 @@ var directiveOrder = []directive{ {"redir", setup.Redir}, {"ext", setup.Ext}, {"basicauth", setup.BasicAuth}, - //{"proxy", setup.Proxy}, - // {"fastcgi", setup.FastCGI}, + {"proxy", setup.Proxy}, + {"fastcgi", setup.FastCGI}, // {"websocket", setup.WebSocket}, // {"markdown", setup.Markdown}, // {"templates", setup.Templates}, diff --git a/config/parse/dispenser.go b/config/parse/dispenser.go new file mode 100644 index 000000000..0f5c19415 --- /dev/null +++ b/config/parse/dispenser.go @@ -0,0 +1,213 @@ +package parse + +import ( + "errors" + "fmt" + "io" + "strings" +) + +// Dispenser is a type that dispenses tokens, similarly to a lexer, +// except that it can do so with some notion of structure and has +// some really convenient methods. +type Dispenser struct { + filename string + tokens []token + cursor int + nesting int +} + +// NewDispenser returns a Dispenser, ready to use for parsing the given input. +func NewDispenser(filename string, input io.Reader) Dispenser { + return Dispenser{ + filename: filename, + tokens: allTokens(input), + cursor: -1, + } +} + +// NewDispenserTokens returns a Dispenser filled with the given tokens. +func NewDispenserTokens(filename string, tokens []token) Dispenser { + return Dispenser{ + filename: filename, + tokens: tokens, + cursor: -1, + } +} + +// Next loads the next token. Returns true if a token +// was loaded; false otherwise. If false, all tokens +// have already been consumed. +func (d *Dispenser) Next() bool { + if d.cursor < len(d.tokens)-1 { + d.cursor++ + return true + } + return false +} + +// 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.cursor < 0 { + d.cursor++ + return true + } + if d.cursor >= len(d.tokens) { + return false + } + if d.cursor < len(d.tokens)-1 && + (d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line) { + d.cursor++ + return true + } + return false +} + +// NextLine loads the next token only if it is not on the same +// line as the current token, and returns true if a token was +// loaded; false otherwise. If false, there is not another token +// or it is on the same line. +func (d *Dispenser) NextLine() bool { + if d.cursor < 0 { + d.cursor++ + return true + } + if d.cursor >= len(d.tokens) { + return false + } + if d.cursor < len(d.tokens)-1 && + d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line { + d.cursor++ + return true + } + return false +} + +// NextBlock can be used as the condition of a for loop +// to load the next token as long as it opens a block or +// is already in a block. It returns true if a token was +// loaded, or false when the block's closing curly brace +// was loaded and thus the block ended. Nested blocks are +// not supported. +func (d *Dispenser) NextBlock() bool { + if d.nesting > 0 { + d.Next() + if d.Val() == "}" { + d.nesting-- + return false + } + return true + } + if !d.NextArg() { // block must open on same line + return false + } + if d.Val() != "{" { + d.cursor-- // roll back if not opening brace + return false + } + d.Next() + d.nesting++ + return true +} + +// Val gets the text of the current token. If there is no token +// loaded, it returns empty string. +func (d *Dispenser) Val() string { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return "" + } + return d.tokens[d.cursor].text +} + +// Line gets the line number of the current token. If there is no token +// loaded, it returns 0. +func (d *Dispenser) Line() int { + if d.cursor < 0 || d.cursor >= len(d.tokens) { + return 0 + } + return d.tokens[d.cursor].line +} + +// 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 +// and false will be returned. If there were enough tokens available +// to fill the arguments, then true will be returned. +func (d *Dispenser) Args(targets ...*string) bool { + enough := true + for i := 0; i < len(targets); i++ { + if !d.NextArg() { + enough = false + break + } + *targets[i] = d.Val() + } + return enough +} + +// RemainingArgs loads any more arguments (tokens on the same line) +// into a slice and returns them. Open curly brace tokens also indicate +// the end of arguments, and the curly brace is not included in +// the return value nor is it loaded. +func (d *Dispenser) RemainingArgs() []string { + var args []string + + for d.NextArg() { + if d.Val() == "{" { + d.cursor-- + break + } + args = append(args, d.Val()) + } + + return args +} + +// ArgErr returns an argument error, meaning that another +// argument was expected but not found. In other words, +// a line break or open curly brace was encountered instead of +// an argument. +func (d *Dispenser) ArgErr() error { + if d.Val() == "{" { + return d.Err("Unexpected token '{', expecting argument") + } + return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val()) +} + +// SyntaxErr creates a generic syntax error which explains what was +// found and what was expected. +func (d *Dispenser) SyntaxErr(expected string) error { + msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.filename, d.Line(), d.Val(), expected) + return errors.New(msg) +} + +// EofErr returns an EOF error, meaning that end of input +// was found when another token was expected. +func (d *Dispenser) EofErr() error { + return d.Errf("Unexpected EOF") +} + +// Err generates a custom parse error with a message of msg. +func (d *Dispenser) Err(msg string) error { + msg = fmt.Sprintf("%s:%d - Parse error: %s", d.filename, d.Line(), msg) + return errors.New(msg) +} + +// Errf is like Err, but for formatted error messages +func (d *Dispenser) Errf(format string, args ...interface{}) error { + return d.Err(fmt.Sprintf(format, args...)) // TODO: I think args needs to be args... +} + +// numLineBreaks counts how many line breaks are in the token +// value given by the token index tknIdx. It returns 0 if the +// token does not exist or there are no line breaks. +func (d *Dispenser) numLineBreaks(tknIdx int) int { + if tknIdx < 0 || tknIdx >= len(d.tokens) { + return 0 + } + return strings.Count(d.tokens[tknIdx].text, "\n") +} diff --git a/config/parse/lexer.go b/config/parse/lexer.go new file mode 100644 index 000000000..5e0e46d54 --- /dev/null +++ b/config/parse/lexer.go @@ -0,0 +1,114 @@ +package parse + +import ( + "bufio" + "io" + "unicode" +) + +type ( + // 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. + lexer struct { + reader *bufio.Reader + token token + line int + } + + // token represents a single parsable unit. + token struct { + line int + text string + } +) + +// load prepares the lexer to scan an input for tokens. +func (l *lexer) load(input io.Reader) error { + l.reader = bufio.NewReader(input) + l.line = 1 + return nil +} + +// 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 + + makeToken := func() bool { + l.token.text = string(val) + return true + } + + for { + ch, _, err := l.reader.ReadRune() + if err != nil { + if len(val) > 0 { + return makeToken() + } + if err == io.EOF { + return false + } else { + panic(err) + } + } + + if quoted { + if !escaped { + if ch == '\\' { + escaped = true + continue + } else if ch == '"' { + quoted = false + return makeToken() + } + } + if ch == '\n' { + l.line++ + } + val = append(val, ch) + escaped = false + continue + } + + if unicode.IsSpace(ch) { + if ch == '\r' { + continue + } + if ch == '\n' { + l.line++ + comment = false + } + if len(val) > 0 { + return makeToken() + } + continue + } + + if ch == '#' { + comment = true + } + + if comment { + continue + } + + if len(val) == 0 { + l.token = token{line: l.line} + if ch == '"' { + quoted = true + continue + } + } + + val = append(val, ch) + } +} diff --git a/config/parse/parse.go b/config/parse/parse.go new file mode 100644 index 000000000..6695abdfc --- /dev/null +++ b/config/parse/parse.go @@ -0,0 +1,27 @@ +// Package parse provides facilities for parsing configuration files. +package parse + +import "io" + +// ServerBlocks parses the input just enough to organize tokens, +// in order, by server block. No further parsing is performed. +// Server blocks are returned in the order in which they appear. +func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) { + p := parser{Dispenser: NewDispenser(filename, input)} + blocks, err := p.parseAll() + return blocks, err +} + +// allTokens lexes the entire input, but does not parse it. +// It returns all the tokens from the input, unstructured +// and in order. +func allTokens(input io.Reader) (tokens []token) { + l := new(lexer) + l.load(input) + for l.next() { + tokens = append(tokens, l.token) + } + return +} + +var ValidDirectives = make(map[string]struct{}) diff --git a/config/parse/parsing.go b/config/parse/parsing.go new file mode 100644 index 000000000..430751107 --- /dev/null +++ b/config/parse/parsing.go @@ -0,0 +1,300 @@ +package parse + +import ( + "net" + "os" + "strings" +) + +type parser struct { + Dispenser + block multiServerBlock // current server block being parsed + eof bool // if we encounter a valid EOF in a hard place +} + +func (p *parser) parseAll() ([]serverBlock, error) { + var blocks []serverBlock + + for p.Next() { + err := p.parseOne() + if err != nil { + return blocks, err + } + + // explode the multiServerBlock into multiple serverBlocks + for _, addr := range p.block.addresses { + blocks = append(blocks, serverBlock{ + Host: addr.host, + Port: addr.port, + Tokens: p.block.tokens, + }) + } + } + + return blocks, nil +} + +func (p *parser) parseOne() error { + p.block = multiServerBlock{tokens: make(map[string][]token)} + + err := p.begin() + if err != nil { + return err + } + + return nil +} + +func (p *parser) begin() error { + err := p.addresses() + if err != nil { + return err + } + + err = p.blockContents() + if err != nil { + return err + } + + return nil +} + +func (p *parser) addresses() error { + var expectingAnother bool + + for { + tkn, startLine := p.Val(), p.Line() + + // Open brace definitely indicates end of addresses + if tkn == "{" { + if expectingAnother { + return p.Errf("Expected another address but had '%s' - check for extra comma", tkn) + } + break + } + + // Trailing comma indicates another address will follow, which + // may possibly be on the next line + if tkn[len(tkn)-1] == ',' { + tkn = tkn[:len(tkn)-1] + expectingAnother = true + } else { + expectingAnother = false // but we may still see another one on this line + } + + // Parse and save this address + host, port, err := standardAddress(tkn) + if err != nil { + return err + } + p.block.addresses = append(p.block.addresses, address{host, port}) + + // Advance token and possibly break out of loop or return error + hasNext := p.Next() + if expectingAnother && !hasNext { + return p.EofErr() + } + if !expectingAnother && p.Line() > startLine { + break + } + if !hasNext { + p.eof = true + break // EOF + } + } + + return nil +} + +func (p *parser) blockContents() error { + errOpenCurlyBrace := p.openCurlyBrace() + if errOpenCurlyBrace != nil { + // single-server configs don't need curly braces + p.cursor-- + } + + if p.eof { + // this happens if the Caddyfile consists of only + // a line of addresses and nothing else + return nil + } + + err := p.directives() + if err != nil { + return err + } + + // Only look for close curly brace if there was an opening + if errOpenCurlyBrace == nil { + err = p.closeCurlyBrace() + if err != nil { + return err + } + } + + return nil +} + +// directives parses through all the lines for directives +// and it expects the next token to be the first +// directive. It goes until EOF or closing curly brace +// which ends the server block. +func (p *parser) directives() error { + for p.Next() { + // end of server block + if p.Val() == "}" { + break + } + + // special case: import directive replaces tokens during parse-time + if p.Val() == "import" { + err := p.doImport() + if err != nil { + return err + } + continue + } + + // normal case: parse a directive on this line + if err := p.directive(); err != nil { + return err + } + } + return nil +} + +// doImport swaps out the import directive and its argument +// (a total of 2 tokens) with the tokens in the file specified. +// When the function returns, the cursor is on the token before +// where the import directive was. In other words, call Next() +// to access the first token that was imported. +func (p *parser) doImport() error { + if !p.NextArg() { + return p.ArgErr() + } + importFile := p.Val() + if p.NextArg() { + return p.Err("Import allows only one file to import") + } + + file, err := os.Open(importFile) + if err != nil { + return p.Errf("Could not import %s - %v", importFile, err) + } + defer file.Close() + importedTokens := allTokens(file) + + // Splice out the import directive and its argument (2 tokens total) + // and insert the imported tokens. + tokensBefore := p.tokens[:p.cursor-1] + tokensAfter := p.tokens[p.cursor+1:] + p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...) + p.cursor -= 2 + + return nil +} + +// directive collects tokens until the directive's scope +// closes (either end of line or end of curly brace block). +// It expects the currently-loaded token to be a directive +// (or } that ends a server block). The collected tokens +// are loaded into the current server block for later use +// by directive setup functions. +func (p *parser) directive() error { + dir := p.Val() + line := p.Line() + nesting := 0 + + if _, ok := ValidDirectives[dir]; !ok { + return p.Errf("Unknown directive '%s'", dir) + } + + // The directive itself is appended as a relevant token + p.block.tokens[dir] = append(p.block.tokens[dir], p.tokens[p.cursor]) + + for p.Next() { + if p.Val() == "{" { + nesting++ + } else if p.Line()+p.numLineBreaks(p.cursor) > line && nesting == 0 { + p.cursor-- // read too far + break + } else if p.Val() == "}" && nesting > 0 { + nesting-- + } else if p.Val() == "}" && nesting == 0 { + return p.Err("Unexpected '}' because no matching opening brace") + } + p.block.tokens[dir] = append(p.block.tokens[dir], p.tokens[p.cursor]) + } + + if nesting > 0 { + return p.EofErr() + } + return nil +} + +// openCurlyBrace expects the current token to be an +// opening curly brace. This acts like an assertion +// because it returns an error if the token is not +// a opening curly brace. It does not advance the token. +func (p *parser) openCurlyBrace() error { + if p.Val() != "{" { + return p.SyntaxErr("{") + } + return nil +} + +// 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. It does not advance the token. +func (p *parser) closeCurlyBrace() error { + if p.Val() != "}" { + return p.SyntaxErr("}") + } + return nil +} + +// standardAddress turns the accepted host and port patterns +// into a format accepted by net.Dial. +func standardAddress(str string) (host, port string, err error) { + var schemePort string + + if strings.HasPrefix(str, "https://") { + schemePort = "https" + str = str[8:] + } else if strings.HasPrefix(str, "http://") { + schemePort = "http" + str = str[7:] + } else if !strings.Contains(str, ":") { + str += ":" // + Port + } + + host, port, err = net.SplitHostPort(str) + if err != nil && schemePort != "" { + host = str + port = schemePort // assume port from scheme + err = nil + } + + return +} + +type ( + // serverBlock stores tokens by directive name for a + // single host:port (address) + serverBlock struct { + Host, Port string + Tokens map[string][]token // directive name to tokens (including directive) + } + + // multiServerBlock is the same as serverBlock but for + // multiple addresses that share the same tokens + multiServerBlock struct { + addresses []address + tokens map[string][]token + } + + address struct { + host, port string + } +) diff --git a/config/setup/basicauth.go b/config/setup/basicauth.go new file mode 100644 index 000000000..6d1ece108 --- /dev/null +++ b/config/setup/basicauth.go @@ -0,0 +1,53 @@ +package setup + +import ( + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/basicauth" +) + +// BasicAuth configures a new BasicAuth middleware instance. +func BasicAuth(c *Controller) (middleware.Middleware, error) { + rules, err := basicAuthParse(c) + if err != nil { + return nil, err + } + + basic := basicauth.BasicAuth{Rules: rules} + + return func(next middleware.Handler) middleware.Handler { + basic.Next = next + return basic + }, nil +} + +func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { + var rules []basicauth.Rule + + for c.Next() { + var rule basicauth.Rule + + args := c.RemainingArgs() + + switch len(args) { + case 2: + rule.Username = args[0] + rule.Password = args[1] + for c.NextBlock() { + rule.Resources = append(rule.Resources, c.Val()) + if c.NextArg() { + return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val()) + } + } + case 3: + rule.Resources = append(rule.Resources, args[0]) + rule.Username = args[1] + rule.Password = args[2] + default: + return rules, c.ArgErr() + } + + rules = append(rules, rule) + } + + return rules, nil +} diff --git a/config/setup/controller.go b/config/setup/controller.go new file mode 100644 index 000000000..5631b8782 --- /dev/null +++ b/config/setup/controller.go @@ -0,0 +1,11 @@ +package setup + +import ( + "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/server" +) + +type Controller struct { + *server.Config + parse.Dispenser +} diff --git a/config/setup/errors.go b/config/setup/errors.go new file mode 100644 index 000000000..94fb4b4ed --- /dev/null +++ b/config/setup/errors.go @@ -0,0 +1,103 @@ +package setup + +import ( + "log" + "os" + "path" + "strconv" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/errors" +) + +// Errors configures a new gzip middleware instance. +func Errors(c *Controller) (middleware.Middleware, error) { + handler, err := errorsParse(c) + if err != nil { + return nil, err + } + + // Open the log file for writing when the server starts + c.Startup = append(c.Startup, func() error { + var err error + var file *os.File + + if handler.LogFile == "stdout" { + file = os.Stdout + } else if handler.LogFile == "stderr" { + file = os.Stderr + } else if handler.LogFile != "" { + file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return err + } + } + + handler.Log = log.New(file, "", 0) + return nil + }) + + return func(next middleware.Handler) middleware.Handler { + handler.Next = next + return handler + }, nil +} + +func errorsParse(c *Controller) (*errors.ErrorHandler, error) { + // Very important that we make a pointer because the Startup + // function that opens the log file must have access to the + // same instance of the handler, not a copy. + handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)} + + optionalBlock := func() (bool, error) { + var hadBlock bool + + for c.NextBlock() { + hadBlock = true + + what := c.Val() + if !c.NextArg() { + return hadBlock, c.ArgErr() + } + where := c.Val() + + if what == "log" { + handler.LogFile = where + } else { + // Error page; ensure it exists + where = path.Join(c.Root, where) + f, err := os.Open(where) + if err != nil { + return hadBlock, c.Err("Unable to open error page '" + where + "': " + err.Error()) + } + f.Close() + + whatInt, err := strconv.Atoi(what) + if err != nil { + return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'") + } + handler.ErrorPages[whatInt] = where + } + } + return hadBlock, nil + } + + for c.Next() { + // Configuration may be in a block + hadBlock, err := optionalBlock() + if err != nil { + return handler, err + } + + // Otherwise, the only argument would be an error log file name + if !hadBlock { + if c.NextArg() { + handler.LogFile = c.Val() + } else { + handler.LogFile = errors.DefaultLogFilename + } + } + } + + return handler, nil +} diff --git a/config/setup/ext.go b/config/setup/ext.go new file mode 100644 index 000000000..4495da664 --- /dev/null +++ b/config/setup/ext.go @@ -0,0 +1,54 @@ +package setup + +import ( + "os" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/extensions" +) + +// Ext configures a new instance of 'extensions' middleware for clean URLs. +func Ext(c *Controller) (middleware.Middleware, error) { + root := c.Root + + exts, err := extParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return extensions.Ext{ + Next: next, + Extensions: exts, + Root: root, + } + }, nil +} + +// extParse sets up an instance of extension middleware +// from a middleware controller and returns a list of extensions. +func extParse(c *Controller) ([]string, error) { + var exts []string + + for c.Next() { + // At least one extension is required + if !c.NextArg() { + return exts, c.ArgErr() + } + exts = append(exts, c.Val()) + + // Tack on any other extensions that may have been listed + exts = append(exts, c.RemainingArgs()...) + } + + return exts, nil +} + +// resourceExists returns true if the file specified at +// root + path exists; false otherwise. +func resourceExists(root, path string) bool { + _, err := os.Stat(root + path) + // technically we should use os.IsNotExist(err) + // but we don't handle any other kinds of errors anyway + return err == nil +} diff --git a/config/setup/fastcgi.go b/config/setup/fastcgi.go new file mode 100644 index 000000000..657affd53 --- /dev/null +++ b/config/setup/fastcgi.go @@ -0,0 +1,105 @@ +package setup + +import ( + "errors" + "path/filepath" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/fastcgi" +) + +// FastCGI configures a new FastCGI middleware instance. +func FastCGI(c *Controller) (middleware.Middleware, error) { + root, err := filepath.Abs(c.Root) + if err != nil { + return nil, err + } + + rules, err := fastcgiParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return fastcgi.Handler{ + Next: next, + Rules: rules, + Root: root, + SoftwareName: "Caddy", // TODO: Once generators are not in the same pkg as handler, obtain this from some global const + SoftwareVersion: "", // TODO: Get this from some global const too + // TODO: Set ServerName and ServerPort to correct values... (as user defined in config) + } + }, nil +} + +func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) { + var rules []fastcgi.Rule + + for c.Next() { + var rule fastcgi.Rule + + args := c.RemainingArgs() + + switch len(args) { + case 0: + return rules, c.ArgErr() + case 1: + rule.Path = "/" + rule.Address = args[0] + case 2: + rule.Path = args[0] + rule.Address = args[1] + case 3: + rule.Path = args[0] + rule.Address = args[1] + err := fastcgiPreset(args[2], &rule) + if err != nil { + return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'") + } + } + + for c.NextBlock() { + switch c.Val() { + case "ext": + if !c.NextArg() { + return rules, c.ArgErr() + } + rule.Ext = c.Val() + case "split": + if !c.NextArg() { + return rules, c.ArgErr() + } + rule.SplitPath = c.Val() + case "index": + if !c.NextArg() { + return rules, c.ArgErr() + } + rule.IndexFile = c.Val() + case "env": + envArgs := c.RemainingArgs() + if len(envArgs) < 2 { + return rules, c.ArgErr() + } + rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]}) + } + } + + rules = append(rules, rule) + } + + return rules, nil +} + +// fastcgiPreset configures rule according to name. It returns an error if +// name is not a recognized preset name. +func fastcgiPreset(name string, rule *fastcgi.Rule) error { + switch name { + case "php": + rule.Ext = ".php" + rule.SplitPath = ".php" + rule.IndexFile = "index.php" + default: + return errors.New(name + " is not a valid preset name") + } + return nil +} diff --git a/config/setup/gzip.go b/config/setup/gzip.go new file mode 100644 index 000000000..aa294de2c --- /dev/null +++ b/config/setup/gzip.go @@ -0,0 +1,13 @@ +package setup + +import ( + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/gzip" +) + +// Gzip configures a new gzip middleware instance. +func Gzip(c *Controller) (middleware.Middleware, error) { + return func(next middleware.Handler) middleware.Handler { + return gzip.Gzip{Next: next} + }, nil +} diff --git a/config/setup/headers.go b/config/setup/headers.go new file mode 100644 index 000000000..3300f0724 --- /dev/null +++ b/config/setup/headers.go @@ -0,0 +1,84 @@ +package setup + +import ( + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/headers" +) + +// Headers configures a new Headers middleware instance. +func Headers(c *Controller) (middleware.Middleware, error) { + rules, err := headersParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return headers.Headers{Next: next, Rules: rules} + }, nil +} + +func headersParse(c *Controller) ([]headers.Rule, error) { + var rules []headers.Rule + + for c.NextLine() { + var head headers.Rule + var isNewPattern bool + + if !c.NextArg() { + return rules, c.ArgErr() + } + pattern := c.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 + } + + for c.NextBlock() { + // A block of headers was opened... + + h := headers.Header{Name: c.Val()} + + if c.NextArg() { + h.Value = c.Val() + } + + head.Headers = append(head.Headers, h) + } + if c.NextArg() { + // ... or single header was defined as an argument instead. + + h := headers.Header{Name: c.Val()} + + h.Value = c.Val() + + if c.NextArg() { + h.Value = c.Val() + } + + head.Headers = append(head.Headers, h) + } + + if isNewPattern { + rules = append(rules, head) + } else { + for i := 0; i < len(rules); i++ { + if rules[i].Url == pattern { + rules[i] = head + break + } + } + } + } + + return rules, nil +} diff --git a/config/setup/log.go b/config/setup/log.go new file mode 100644 index 000000000..32b5edba0 --- /dev/null +++ b/config/setup/log.go @@ -0,0 +1,90 @@ +package setup + +import ( + "log" + "os" + + "github.com/mholt/caddy/middleware" + caddylog "github.com/mholt/caddy/middleware/log" +) + +func Log(c *Controller) (middleware.Middleware, error) { + rules, err := logParse(c) + if err != nil { + return nil, err + } + + // Open the log files for writing when the server starts + c.Startup = append(c.Startup, func() error { + for i := 0; i < len(rules); i++ { + var err error + var file *os.File + + if rules[i].OutputFile == "stdout" { + file = os.Stdout + } else if rules[i].OutputFile == "stderr" { + file = os.Stderr + } else { + file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return err + } + } + + rules[i].Log = log.New(file, "", 0) + } + + return nil + }) + + return func(next middleware.Handler) middleware.Handler { + return caddylog.Logger{Next: next, Rules: rules} + }, nil +} + +func logParse(c *Controller) ([]caddylog.LogRule, error) { + var rules []caddylog.LogRule + + for c.Next() { + args := c.RemainingArgs() + + if len(args) == 0 { + // Nothing specified; use defaults + rules = append(rules, caddylog.LogRule{ + PathScope: "/", + OutputFile: caddylog.DefaultLogFilename, + Format: caddylog.DefaultLogFormat, + }) + } else if len(args) == 1 { + // Only an output file specified + rules = append(rules, caddylog.LogRule{ + PathScope: "/", + OutputFile: args[0], + Format: caddylog.DefaultLogFormat, + }) + } else { + // Path scope, output file, and maybe a format specified + + format := caddylog.DefaultLogFormat + + if len(args) > 2 { + switch args[2] { + case "{common}": + format = caddylog.CommonLogFormat + case "{combined}": + format = caddylog.CombinedLogFormat + default: + format = args[2] + } + } + + rules = append(rules, caddylog.LogRule{ + PathScope: args[0], + OutputFile: args[1], + Format: format, + }) + } + } + + return rules, nil +} diff --git a/config/setup/proxy.go b/config/setup/proxy.go new file mode 100644 index 000000000..b993763e9 --- /dev/null +++ b/config/setup/proxy.go @@ -0,0 +1,145 @@ +package setup + +import ( + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/proxy" +) + +// Proxy configures a new Proxy middleware instance. +func Proxy(c *Controller) (middleware.Middleware, error) { + if upstreams, err := newStaticUpstreams(c); err == nil { + return func(next middleware.Handler) middleware.Handler { + return proxy.Proxy{Next: next, Upstreams: upstreams} + }, nil + } else { + return nil, err + } +} + +// newStaticUpstreams parses the configuration input and sets up +// static upstreams for the proxy middleware. +func newStaticUpstreams(c *Controller) ([]proxy.Upstream, error) { + var upstreams []proxy.Upstream + + for c.Next() { + upstream := &proxy.StaticUpstream{ + From: "", + Hosts: nil, + Policy: &proxy.Random{}, + FailTimeout: 10 * time.Second, + MaxFails: 1, + } + var proxyHeaders http.Header + if !c.Args(&upstream.From) { + return upstreams, c.ArgErr() + } + to := c.RemainingArgs() + if len(to) == 0 { + return upstreams, c.ArgErr() + } + + for c.NextBlock() { + switch c.Val() { + case "policy": + if !c.NextArg() { + return upstreams, c.ArgErr() + } + switch c.Val() { + case "random": + upstream.Policy = &proxy.Random{} + case "round_robin": + upstream.Policy = &proxy.RoundRobin{} + case "least_conn": + upstream.Policy = &proxy.LeastConn{} + default: + return upstreams, c.ArgErr() + } + case "fail_timeout": + if !c.NextArg() { + return upstreams, c.ArgErr() + } + if dur, err := time.ParseDuration(c.Val()); err == nil { + upstream.FailTimeout = dur + } else { + return upstreams, err + } + case "max_fails": + if !c.NextArg() { + return upstreams, c.ArgErr() + } + if n, err := strconv.Atoi(c.Val()); err == nil { + upstream.MaxFails = int32(n) + } else { + return upstreams, err + } + case "health_check": + if !c.NextArg() { + return upstreams, c.ArgErr() + } + upstream.HealthCheck.Path = c.Val() + upstream.HealthCheck.Interval = 30 * time.Second + if c.NextArg() { + if dur, err := time.ParseDuration(c.Val()); err == nil { + upstream.HealthCheck.Interval = dur + } else { + return upstreams, err + } + } + case "proxy_header": + var header, value string + if !c.Args(&header, &value) { + return upstreams, c.ArgErr() + } + if proxyHeaders == nil { + proxyHeaders = make(map[string][]string) + } + proxyHeaders.Add(header, value) + } + } + + upstream.Hosts = make([]*proxy.UpstreamHost, len(to)) + for i, host := range to { + if !strings.HasPrefix(host, "http") { + host = "http://" + host + } + uh := &proxy.UpstreamHost{ + Name: host, + Conns: 0, + Fails: 0, + FailTimeout: upstream.FailTimeout, + Unhealthy: false, + ExtraHeaders: proxyHeaders, + CheckDown: func(upstream *proxy.StaticUpstream) proxy.UpstreamHostDownFunc { + return func(uh *proxy.UpstreamHost) bool { + if uh.Unhealthy { + return true + } + if uh.Fails >= upstream.MaxFails && + upstream.MaxFails != 0 { + return true + } + return false + } + }(upstream), + } + if baseUrl, err := url.Parse(uh.Name); err == nil { + uh.ReverseProxy = proxy.NewSingleHostReverseProxy(baseUrl) + } else { + return upstreams, err + } + upstream.Hosts[i] = uh + } + + if upstream.HealthCheck.Path != "" { + go upstream.HealthCheckWorker(nil) + } + upstreams = append(upstreams, upstream) + } + return upstreams, nil +} diff --git a/config/setup/redir.go b/config/setup/redir.go new file mode 100644 index 000000000..23a23eb15 --- /dev/null +++ b/config/setup/redir.go @@ -0,0 +1,77 @@ +package setup + +import ( + "net/http" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/redirect" +) + +// Redir configures a new Redirect middleware instance. +func Redir(c *Controller) (middleware.Middleware, error) { + rules, err := redirParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return redirect.Redirect{Next: next, Rules: rules} + }, nil +} + +func redirParse(c *Controller) ([]redirect.Rule, error) { + var redirects []redirect.Rule + + for c.Next() { + var rule redirect.Rule + args := c.RemainingArgs() + + switch len(args) { + case 1: + // To specified + rule.From = "/" + rule.To = args[0] + rule.Code = http.StatusMovedPermanently + case 2: + // To and Code specified + rule.From = "/" + rule.To = args[0] + if code, ok := httpRedirs[args[1]]; !ok { + return redirects, c.Err("Invalid redirect code '" + args[1] + "'") + } else { + rule.Code = code + } + case 3: + // From, To, and Code specified + rule.From = args[0] + rule.To = args[1] + if code, ok := httpRedirs[args[2]]; !ok { + return redirects, c.Err("Invalid redirect code '" + args[2] + "'") + } else { + rule.Code = code + } + default: + return redirects, c.ArgErr() + } + + if rule.From == rule.To { + return redirects, c.Err("Redirect rule cannot allow From and To arguments to be the same.") + } + + redirects = append(redirects, rule) + } + + return redirects, nil +} + +// 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, + "307": 307, + "308": 308, +} diff --git a/config/setup/rewrite.go b/config/setup/rewrite.go new file mode 100644 index 000000000..b86be3036 --- /dev/null +++ b/config/setup/rewrite.go @@ -0,0 +1,40 @@ +package setup + +import ( + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/rewrite" +) + +// Rewrite configures a new Rewrite middleware instance. +func Rewrite(c *Controller) (middleware.Middleware, error) { + rewrites, err := rewriteParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return rewrite.Rewrite{Next: next, Rules: rewrites} + }, nil +} + +func rewriteParse(c *Controller) ([]rewrite.Rule, error) { + var rewrites []rewrite.Rule + + for c.Next() { + var rule rewrite.Rule + + if !c.NextArg() { + return rewrites, c.ArgErr() + } + rule.From = c.Val() + + if !c.NextArg() { + return rewrites, c.ArgErr() + } + rule.To = c.Val() + + rewrites = append(rewrites, rule) + } + + return rewrites, nil +} diff --git a/config/setup/root.go b/config/setup/root.go new file mode 100644 index 000000000..892578b7a --- /dev/null +++ b/config/setup/root.go @@ -0,0 +1,31 @@ +package setup + +import ( + "log" + "os" + + "github.com/mholt/caddy/middleware" +) + +func Root(c *Controller) (middleware.Middleware, error) { + for c.Next() { + if !c.NextArg() { + return nil, c.ArgErr() + } + c.Root = c.Val() + } + + // Check if root path exists + _, err := os.Stat(c.Root) + if err != nil { + if os.IsNotExist(err) { + // Allow this, because the folder might appear later. + // But make sure the user knows! + log.Printf("Warning: Root path does not exist: %s", c.Root) + } else { + return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err) + } + } + + return nil, nil +} diff --git a/config/setup/startupshutdown.go b/config/setup/startupshutdown.go new file mode 100644 index 000000000..e34357760 --- /dev/null +++ b/config/setup/startupshutdown.go @@ -0,0 +1,43 @@ +package setup + +import ( + "os" + "os/exec" + + "github.com/mholt/caddy/middleware" +) + +func Startup(c *Controller) (middleware.Middleware, error) { + return nil, registerCallback(c, &c.Startup) +} + +func Shutdown(c *Controller) (middleware.Middleware, error) { + return nil, registerCallback(c, &c.Shutdown) +} + +// registerCallback registers a callback function to execute by +// using c to parse the line. It appends the callback function +// to the list of callback functions passed in by reference. +func registerCallback(c *Controller, list *[]func() error) error { + for c.Next() { + if !c.NextArg() { + return c.ArgErr() + } + + command, args, err := middleware.SplitCommandAndArgs(c.Val()) + if err != nil { + return c.Err(err.Error()) + } + + fn := func() error { + cmd := exec.Command(command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + } + + *list = append(*list, fn) + } + + return nil +} diff --git a/config/setup/tls.go b/config/setup/tls.go new file mode 100644 index 000000000..ea2675345 --- /dev/null +++ b/config/setup/tls.go @@ -0,0 +1,21 @@ +package setup + +import "github.com/mholt/caddy/middleware" + +func TLS(c *Controller) (middleware.Middleware, error) { + c.TLS.Enabled = true + + for c.Next() { + if !c.NextArg() { + return nil, c.ArgErr() + } + c.TLS.Certificate = c.Val() + + if !c.NextArg() { + return nil, c.ArgErr() + } + c.TLS.Key = c.Val() + } + + return nil, nil +} diff --git a/middleware/fastcgi/fastcgi.go b/middleware/fastcgi/fastcgi.go index 7d3837a0a..cc811a061 100644 --- a/middleware/fastcgi/fastcgi.go +++ b/middleware/fastcgi/fastcgi.go @@ -4,7 +4,6 @@ package fastcgi import ( - "errors" "io" "net/http" "os" @@ -15,30 +14,6 @@ import ( "github.com/mholt/caddy/middleware" ) -// New generates a new FastCGI middleware. -func New(c middleware.Controller) (middleware.Middleware, error) { - root, err := filepath.Abs(c.Root()) - if err != nil { - return nil, err - } - - rules, err := parse(c) - if err != nil { - return nil, err - } - - return func(next middleware.Handler) middleware.Handler { - return Handler{ - Next: next, - Rules: rules, - Root: root, - SoftwareName: "Caddy", // TODO: Once generators are not in the same pkg as handler, obtain this from some global const - SoftwareVersion: "", // TODO: Get this from some global const too - // TODO: Set ServerName and ServerPort to correct values... (as user defined in config) - } - }, nil -} - // Handler is a middleware type that can handle requests as a FastCGI client. type Handler struct { Next middleware.Handler @@ -222,78 +197,6 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, path string) (map[string]s return env, nil } -func parse(c middleware.Controller) ([]Rule, error) { - var rules []Rule - - for c.Next() { - var rule Rule - - args := c.RemainingArgs() - - switch len(args) { - case 0: - return rules, c.ArgErr() - case 1: - rule.Path = "/" - rule.Address = args[0] - case 2: - rule.Path = args[0] - rule.Address = args[1] - case 3: - rule.Path = args[0] - rule.Address = args[1] - err := preset(args[2], &rule) - if err != nil { - return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'") - } - } - - for c.NextBlock() { - switch c.Val() { - case "ext": - if !c.NextArg() { - return rules, c.ArgErr() - } - rule.Ext = c.Val() - case "split": - if !c.NextArg() { - return rules, c.ArgErr() - } - rule.SplitPath = c.Val() - case "index": - if !c.NextArg() { - return rules, c.ArgErr() - } - rule.IndexFile = c.Val() - case "env": - envArgs := c.RemainingArgs() - if len(envArgs) < 2 { - return rules, c.ArgErr() - } - rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]}) - } - } - - rules = append(rules, rule) - } - - return rules, nil -} - -// preset configures rule according to name. It returns an error if -// name is not a recognized preset name. -func preset(name string, rule *Rule) error { - switch name { - case "php": - rule.Ext = ".php" - rule.SplitPath = ".php" - rule.IndexFile = "index.php" - default: - return errors.New(name + " is not a valid preset name") - } - return nil -} - // Rule represents a FastCGI handling rule. type Rule struct { // The base path to match. Required. diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go index 083c27054..e04f422de 100644 --- a/middleware/proxy/proxy.go +++ b/middleware/proxy/proxy.go @@ -3,11 +3,12 @@ package proxy import ( "errors" - "github.com/mholt/caddy/middleware" "net/http" "net/url" "sync/atomic" "time" + + "github.com/mholt/caddy/middleware" ) var errUnreachable = errors.New("Unreachable backend") @@ -21,8 +22,8 @@ type Proxy struct { // An upstream manages a pool of proxy upstream hosts. Select should return a // suitable upstream host, or nil if no such hosts are available. type Upstream interface { - // The path this upstream host should be routed on - From() string + //The path this upstream host should be routed on + from() string // Selects an upstream host to be routed to. Select() *UpstreamHost } @@ -54,7 +55,7 @@ func (uh *UpstreamHost) Down() bool { func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, upstream := range p.Upstreams { - if middleware.Path(r.URL.Path).Matches(upstream.From()) { + if middleware.Path(r.URL.Path).Matches(upstream.from()) { var replacer middleware.Replacer start := time.Now() requestHost := r.Host @@ -119,14 +120,3 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { return p.Next.ServeHTTP(w, r) } - -// New creates a new instance of proxy middleware. -func New(c middleware.Controller) (middleware.Middleware, error) { - if upstreams, err := newStaticUpstreams(c); err == nil { - return func(next middleware.Handler) middleware.Handler { - return Proxy{Next: next, Upstreams: upstreams} - }, nil - } else { - return nil, err - } -} diff --git a/middleware/proxy/upstream.go b/middleware/proxy/upstream.go index a01002090..55da12db1 100644 --- a/middleware/proxy/upstream.go +++ b/middleware/proxy/upstream.go @@ -1,18 +1,14 @@ package proxy import ( - "github.com/mholt/caddy/middleware" "io" "io/ioutil" "net/http" - "net/url" - "strconv" - "strings" "time" ) -type staticUpstream struct { - from string +type StaticUpstream struct { + From string Hosts HostPool Policy Policy @@ -24,127 +20,11 @@ type staticUpstream struct { } } -func newStaticUpstreams(c middleware.Controller) ([]Upstream, error) { - var upstreams []Upstream - - for c.Next() { - upstream := &staticUpstream{ - from: "", - Hosts: nil, - Policy: &Random{}, - FailTimeout: 10 * time.Second, - MaxFails: 1, - } - var proxyHeaders http.Header - if !c.Args(&upstream.from) { - return upstreams, c.ArgErr() - } - to := c.RemainingArgs() - if len(to) == 0 { - return upstreams, c.ArgErr() - } - - for c.NextBlock() { - switch c.Val() { - case "policy": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - switch c.Val() { - case "random": - upstream.Policy = &Random{} - case "round_robin": - upstream.Policy = &RoundRobin{} - case "least_conn": - upstream.Policy = &LeastConn{} - default: - return upstreams, c.ArgErr() - } - case "fail_timeout": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - if dur, err := time.ParseDuration(c.Val()); err == nil { - upstream.FailTimeout = dur - } else { - return upstreams, err - } - case "max_fails": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - if n, err := strconv.Atoi(c.Val()); err == nil { - upstream.MaxFails = int32(n) - } else { - return upstreams, err - } - case "health_check": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - upstream.HealthCheck.Path = c.Val() - upstream.HealthCheck.Interval = 30 * time.Second - if c.NextArg() { - if dur, err := time.ParseDuration(c.Val()); err == nil { - upstream.HealthCheck.Interval = dur - } else { - return upstreams, err - } - } - case "proxy_header": - var header, value string - if !c.Args(&header, &value) { - return upstreams, c.ArgErr() - } - if proxyHeaders == nil { - proxyHeaders = make(map[string][]string) - } - proxyHeaders.Add(header, value) - } - } - - upstream.Hosts = make([]*UpstreamHost, len(to)) - for i, host := range to { - if !strings.HasPrefix(host, "http") { - host = "http://" + host - } - uh := &UpstreamHost{ - Name: host, - Conns: 0, - Fails: 0, - FailTimeout: upstream.FailTimeout, - Unhealthy: false, - ExtraHeaders: proxyHeaders, - CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc { - return func(uh *UpstreamHost) bool { - if uh.Unhealthy { - return true - } - if uh.Fails >= upstream.MaxFails && - upstream.MaxFails != 0 { - return true - } - return false - } - }(upstream), - } - if baseUrl, err := url.Parse(uh.Name); err == nil { - uh.ReverseProxy = NewSingleHostReverseProxy(baseUrl) - } else { - return upstreams, err - } - upstream.Hosts[i] = uh - } - - if upstream.HealthCheck.Path != "" { - go upstream.healthCheckWorker(nil) - } - upstreams = append(upstreams, upstream) - } - return upstreams, nil +func (u *StaticUpstream) from() string { + return u.From } -func (u *staticUpstream) healthCheck() { +func (u *StaticUpstream) healthCheck() { for _, host := range u.Hosts { hostUrl := host.Name + u.HealthCheck.Path if r, err := http.Get(hostUrl); err == nil { @@ -157,7 +37,7 @@ func (u *staticUpstream) healthCheck() { } } -func (u *staticUpstream) healthCheckWorker(stop chan struct{}) { +func (u *StaticUpstream) HealthCheckWorker(stop chan struct{}) { ticker := time.NewTicker(u.HealthCheck.Interval) u.healthCheck() for { @@ -172,11 +52,7 @@ func (u *staticUpstream) healthCheckWorker(stop chan struct{}) { } } -func (u *staticUpstream) From() string { - return u.from -} - -func (u *staticUpstream) Select() *UpstreamHost { +func (u *StaticUpstream) Select() *UpstreamHost { pool := u.Hosts if len(pool) == 1 { if pool[0].Down() { diff --git a/server/config.go b/server/config.go new file mode 100644 index 000000000..40ba55e98 --- /dev/null +++ b/server/config.go @@ -0,0 +1,50 @@ +package server + +import ( + "net" + + "github.com/mholt/caddy/middleware" +) + +// Config configuration for a single server. +type Config struct { + // The hostname or IP on which to serve + Host string + + // The port to listen on + Port string + + // The directory from which to serve files + Root string + + // HTTPS configuration + TLS TLSConfig + + // Middleware stack; map of path scope to middleware -- TODO: Support path scope? + Middleware map[string][]middleware.Middleware + + // Functions (or methods) to execute at server start; these + // are executed before any parts of the server are configured, + // and the functions are blocking + Startup []func() error + + // Functions (or methods) to execute when the server quits; + // these are executed in response to SIGINT and are blocking + Shutdown []func() error + + // The path to the configuration file from which this was loaded + ConfigFile string +} + +// Address returns the host:port of c as a string. +func (c Config) Address() string { + return net.JoinHostPort(c.Host, c.Port) +} + +// TLSConfig describes how TLS should be configured and used, +// if at all. A certificate and key are both required. +type TLSConfig struct { + Enabled bool + Certificate string + Key string +}