Bringing in latest from master; refactoring under way

This commit is contained in:
Matthew Holt 2015-05-04 06:53:54 -06:00
parent 5f32f9b1c8
commit 995edf0566
26 changed files with 171 additions and 2428 deletions

View File

@ -1,13 +1,13 @@
// Package config contains utilities and types necessary for
// launching specially-configured server instances.
package config
import (
"io"
"log"
"net"
"os"
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
const (
@ -19,110 +19,89 @@ const (
DefaultConfigFile = "Caddyfile"
)
// Host and Port are configurable via command line flag
// These three defaults are configurable through the command line
var (
Root = DefaultRoot
Host = DefaultHost
Port = DefaultPort
)
// config represents a server configuration. It
// is populated by parsing a config file (via the
// Load function).
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
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
}
// 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) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
func Load(filename string, input io.Reader) ([]server.Config, error) {
var configs []server.Config
// turn off timestamp for parsing
flags := log.Flags()
log.SetFlags(0)
p, err := newParser(file)
serverBlocks, err := parse.ServerBlocks(filename, input)
if err != nil {
return nil, err
return configs, err
}
cfgs, err := p.parse()
if err != nil {
return []Config{}, err
}
// Each server block represents a single server/address.
// Iterate each server block and make a config for each one,
// executing the directives that were parsed.
for _, sb := range serverBlocks {
config := server.Config{
Host: sb.Host,
Port: sb.Port,
Middleware: make(map[string][]middleware.Middleware),
}
for i := 0; i < len(cfgs); i++ {
cfgs[i].ConfigFile = filename
// It is crucial that directives are executed in the proper order.
for _, dir := range directiveOrder {
// Execute directive if it is in the server block
if tokens, ok := sb.Tokens[dir.name]; ok {
// Each setup function gets a controller, which is the
// server config and the dispenser containing only
// this directive's tokens.
controller := &setup.Controller{
Config: &config,
Dispenser: parse.NewDispenserTokens(filename, tokens),
}
midware, err := dir.setup(controller)
if err != nil {
return configs, err
}
if midware != nil {
// TODO: For now, we only support the default path scope /
config.Middleware["/"] = append(config.Middleware["/"], midware)
}
}
}
if config.Port == "" {
config.Port = Port
}
configs = append(configs, config)
}
// restore logging settings
log.SetFlags(flags)
return cfgs, nil
return configs, nil
}
// IsNotFound returns whether or not the error is
// one which indicates that the configuration file
// was not found. (Useful for checking the error
// returned from Load).
func IsNotFound(err error) bool {
return os.IsNotExist(err)
}
// Default makes a default configuration
// that's empty except for root, host, and port,
// which are essential for serving the cwd.
func Default() []Config {
cfg := []Config{
Config{
Root: DefaultRoot,
Host: Host,
Port: Port,
},
// validDirective returns true if d is a valid
// directive; false otherwise.
func validDirective(d string) bool {
for _, dir := range directiveOrder {
if dir.name == d {
return true
}
}
return false
}
// Default makes a default configuration which
// is empty except for root, host, and port,
// which are essentials for serving the cwd.
func Default() server.Config {
return server.Config{
Root: Root,
Host: Host,
Port: Port,
}
return cfg
}

View File

@ -1,48 +0,0 @@
package config
import "github.com/mholt/caddy/middleware"
// controller is a dispenser of tokens and also
// facilitates setup with the server by providing
// access to its configuration. It implements
// the middleware.Controller interface.
type controller struct {
dispenser
parser *parser
pathScope string
}
// newController returns a new controller.
func newController(p *parser) *controller {
return &controller{
dispenser: dispenser{
cursor: -1,
filename: p.filename,
},
parser: p,
}
}
// Startup registers a function to execute when the server starts.
func (c *controller) Startup(fn func() error) {
c.parser.cfg.Startup = append(c.parser.cfg.Startup, fn)
}
// Shutdown registers a function to execute when the server exits.
func (c *controller) Shutdown(fn func() error) {
c.parser.cfg.Shutdown = append(c.parser.cfg.Shutdown, fn)
}
// Root returns the server root file path.
func (c *controller) Root() string {
if c.parser.cfg.Root == "" {
return "."
} else {
return c.parser.cfg.Root
}
}
// Context returns the path scope that the Controller is in.
func (c *controller) Context() middleware.Path {
return middleware.Path(c.pathScope)
}

View File

@ -1,37 +0,0 @@
package config
import "testing"
func TestController(t *testing.T) {
p := &parser{filename: "test"}
c := newController(p)
if c == nil || c.parser == nil {
t.Fatal("Expected newController to return a non-nil controller with a non-nil parser")
}
if c.dispenser.cursor != -1 {
t.Errorf("Dispenser not initialized properly; expecting cursor at -1, got %d", c.dispenser.cursor)
}
if c.dispenser.filename != p.filename {
t.Errorf("Dispenser's filename should be same as parser's (%s); got '%s'", p.filename, c.dispenser.filename)
}
c.Startup(func() error { return nil })
if n := len(c.parser.cfg.Startup); n != 1 {
t.Errorf("Expected length of startup functions to be 1, got %d", n)
}
if root := c.Root(); root != "." {
t.Errorf("Expected defualt root path to be '.', got '%s'", root)
}
c.parser.cfg.Root = "foobar/test"
if root := c.Root(); root != c.parser.cfg.Root {
t.Errorf("Expected established root path to be '%s', got '%s'", c.parser.cfg.Root, root)
}
c.pathScope = "unused"
if context := c.Context(); string(context) != c.pathScope {
t.Errorf("Expected context to be '%s', got '%s'", c.pathScope, context)
}
}

View File

@ -1,139 +1,45 @@
package config
import (
"fmt"
"log"
"os"
"os/exec"
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/middleware"
)
// dirFunc is a type of parsing function which processes
// a particular directive and populates the config.
type dirFunc func(*parser) error
// validDirectives is a map of valid, built-in directive names
// to their parsing function. Built-in directives cannot be
// ordered, so they should only be used for internal server
// configuration; not directly handling requests.
var validDirectives map[string]dirFunc
func init() {
// This has to be in the init function
// to avoid an initialization loop error because
// the 'import' directive (key) in this map
// invokes a method that uses this map.
validDirectives = map[string]dirFunc{
"root": func(p *parser) error {
if !p.nextArg() {
return p.argErr()
}
p.cfg.Root = p.tkn()
// Ensure root folder exists
_, err := os.Stat(p.cfg.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 %s does not exist", p.cfg.Root)
} else {
return p.err("Path", fmt.Sprintf("Unable to access root path '%s': %s", p.cfg.Root, err.Error()))
}
}
return nil
},
"import": func(p *parser) error {
if !p.nextArg() {
return p.argErr()
}
filename := p.tkn()
file, err := os.Open(filename)
if err != nil {
return p.err("Parse", err.Error())
}
defer file.Close()
p2, err := newParser(file)
if err != nil {
return p.err("Parse", "Could not import "+filename+"; "+err.Error())
}
p2.cfg = p.cfg
err = p2.directives()
if err != nil {
return err
}
p.cfg = p2.cfg
return nil
},
"tls": func(p *parser) error {
tls := TLSConfig{Enabled: true}
if !p.nextArg() {
return p.argErr()
}
tls.Certificate = p.tkn()
if !p.nextArg() {
return p.argErr()
}
tls.Key = p.tkn()
p.cfg.TLS = tls
return nil
},
"startup": func(p *parser) error {
// TODO: This code is duplicated with the shutdown directive below
if !p.nextArg() {
return p.argErr()
}
command, args, err := middleware.SplitCommandAndArgs(p.tkn())
if err != nil {
return p.err("Parse", err.Error())
}
startupfn := func() error {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
return nil
}
p.cfg.Startup = append(p.cfg.Startup, startupfn)
return nil
},
"shutdown": func(p *parser) error {
if !p.nextArg() {
return p.argErr()
}
command, args, err := middleware.SplitCommandAndArgs(p.tkn())
if err != nil {
return p.err("Parse", err.Error())
}
shutdownfn := func() error {
cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
return nil
}
p.cfg.Shutdown = append(p.cfg.Shutdown, shutdownfn)
return nil
},
// The parse package must know which directives
// are valid, but it must not import the setup
// or config package.
for _, dir := range directiveOrder {
parse.ValidDirectives[dir.name] = struct{}{}
}
}
var directiveOrder = []directive{
{"root", setup.Root},
{"tls", setup.TLS},
{"startup", setup.Startup},
{"shutdown", setup.Shutdown},
{"log", setup.Log},
{"gzip", setup.Gzip},
{"errors", setup.Errors},
{"header", setup.Headers},
{"rewrite", setup.Rewrite},
{"redir", setup.Redir},
{"ext", setup.Ext},
{"basicauth", setup.BasicAuth},
//{"proxy", setup.Proxy},
// {"fastcgi", setup.FastCGI},
// {"websocket", setup.WebSocket},
// {"markdown", setup.Markdown},
// {"templates", setup.Templates},
// {"browse", setup.Browse},
}
type directive struct {
name string
setup setupFunc
}
type setupFunc func(c *setup.Controller) (middleware.Middleware, error)

View File

@ -1,159 +0,0 @@
package config
import (
"errors"
"fmt"
)
// dispenser is a type that dispenses tokens, similarly to
// a lexer, except that it can do so with some notion of
// structure. Its methods implement part of the
// middleware.Controller interface, so refer to that
// documentation for more info.
type dispenser struct {
filename string
cursor int
nesting int
tokens []token
}
// 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.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.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 (currently) 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 ""
} else {
return d.tokens[d.cursor].text
}
}
// 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.Err("Wrong argument count or unexpected line ending after '" + d.Val() + "'")
}
// 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.tokens[d.cursor].line, msg)
return errors.New(msg)
}

View File

@ -1,310 +0,0 @@
package config
import (
"reflect"
"strings"
"testing"
)
func TestDispenser_Val_Next(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3
dir3`
d := makeTestDispenser("test", input)
if val := d.Val(); val != "" {
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
}
assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) {
if loaded := d.Next(); loaded != shouldLoad {
t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor)
}
if d.nesting != 0 {
t.Errorf("Nesting should be 0, was %d instead", d.nesting)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
}
assertNext(true, 0, "host:port")
assertNext(true, 1, "dir1")
assertNext(true, 2, "arg1")
assertNext(true, 3, "dir2")
assertNext(true, 4, "arg2")
assertNext(true, 5, "arg3")
assertNext(true, 6, "dir3")
// Note: This next test simply asserts existing behavior.
// If desired, we may wish to empty the token value after
// reading past the EOF. Open an issue if you want this change.
assertNext(false, 6, "dir3")
}
func TestDispenser_NextArg(t *testing.T) {
input := `dir1 arg1
dir2 arg2 arg3
dir3`
d := makeTestDispenser("test", input)
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.Next() != shouldLoad {
t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
}
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
if d.NextArg() != true {
t.Error("NextArg(): Should load next argument but got false instead")
}
if d.cursor != expectedCursor {
t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
if !loadAnother {
if d.NextArg() != false {
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor)
}
}
}
assertNext(true, "dir1", 0)
assertNextArg("arg1", false, 1)
assertNext(true, "dir2", 2)
assertNextArg("arg2", true, 3)
assertNextArg("arg3", false, 4)
assertNext(true, "dir3", 5)
assertNext(false, "dir3", 5)
}
func TestDispenser_NextLine(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3`
d := makeTestDispenser("test", input)
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.NextLine() != shouldLoad {
t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor)
}
if val := d.Val(); val != expectedVal {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
}
assertNextLine(true, "host:port", 0)
assertNextLine(true, "dir1", 1)
assertNextLine(false, "dir1", 1)
d.Next() // arg1
assertNextLine(true, "dir2", 3)
assertNextLine(false, "dir2", 3)
d.Next() // arg2
assertNextLine(false, "arg2", 4)
d.Next() // arg3
assertNextLine(false, "arg3", 5)
}
func TestDispenser_NextBlock(t *testing.T) {
input := `foobar1 {
sub1 arg1
sub2
}
foobar2 {
}`
d := makeTestDispenser("test", input)
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
if loaded := d.NextBlock(); loaded != shouldLoad {
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
}
if d.cursor != expectedCursor {
t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor)
}
if d.nesting != expectedNesting {
t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting)
}
}
assertNextBlock(false, -1, 0)
d.Next() // foobar1
assertNextBlock(true, 2, 1)
assertNextBlock(true, 3, 1)
assertNextBlock(true, 4, 1)
assertNextBlock(false, 5, 0)
d.Next() // foobar2
assertNextBlock(true, 8, 1)
assertNextBlock(false, 8, 0)
}
func TestDispenser_Args(t *testing.T) {
var s1, s2, s3 string
input := `dir1 arg1 arg2 arg3
dir2 arg4 arg5
dir3 arg6 arg7
dir4`
d := makeTestDispenser("test", input)
d.Next() // dir1
// As many strings as arguments
if all := d.Args(&s1, &s2, &s3); !all {
t.Error("Args(): Expected true, got false")
}
if s1 != "arg1" {
t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1)
}
if s2 != "arg2" {
t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2)
}
if s3 != "arg3" {
t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3)
}
d.Next() // dir2
// More strings than arguments
if all := d.Args(&s1, &s2, &s3); all {
t.Error("Args(): Expected false, got true")
}
if s1 != "arg4" {
t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1)
}
if s2 != "arg5" {
t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2)
}
if s3 != "arg3" {
t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3)
}
// (quick cursor check just for kicks and giggles)
if d.cursor != 6 {
t.Errorf("Cursor should be 6, but is %d", d.cursor)
}
d.Next() // dir3
// More arguments than strings
if all := d.Args(&s1); !all {
t.Error("Args(): Expected true, got false")
}
if s1 != "arg6" {
t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1)
}
d.Next() // dir4
// No arguments or strings
if all := d.Args(); !all {
t.Error("Args(): Expected true, got false")
}
// No arguments but at least one string
if all := d.Args(&s1); all {
t.Error("Args(): Expected false, got true")
}
}
func TestDispenser_RemainingArgs(t *testing.T) {
input := `dir1 arg1 arg2 arg3
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := makeTestDispenser("test", input)
d.Next() // dir1
args := d.RemainingArgs()
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) {
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
}
d.Next() // dir2
args = d.RemainingArgs()
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) {
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
}
d.Next() // dir3
args = d.RemainingArgs()
if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) {
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
}
d.Next() // {
d.Next() // arg7
d.Next() // dir4
args = d.RemainingArgs()
if len(args) != 0 {
t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args)
}
}
func TestDispenser_ArgErr_Err(t *testing.T) {
input := `dir1 {
}
dir2 arg1 arg2`
d := makeTestDispenser("test", input)
d.cursor = 1 // {
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") {
t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err)
}
d.cursor = 5 // arg2
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") {
t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err)
}
err := d.Err("foobar")
if err == nil {
t.Fatalf("Err(): Expected an error, got nil")
}
if !strings.Contains(err.Error(), "test:3") {
t.Errorf("Expected error message with filename:line in it; got '%v'", err)
}
if !strings.Contains(err.Error(), "foobar") {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
}
func makeTestDispenser(filename, input string) dispenser {
return dispenser{
filename: filename,
cursor: -1,
tokens: getTokens(input),
}
}
func getTokens(input string) (tokens []token) {
var l lexer
l.load(strings.NewReader(input))
for l.next() {
tokens = append(tokens, l.token)
}
return
}

View File

@ -1 +0,0 @@
root /test/imported/public_html

View File

@ -1,114 +0,0 @@
package config
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 processable unit.
token struct {
line int
text string
}
)
// 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
}
// 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)
}
}

View File

@ -1,139 +0,0 @@
package config
import (
"strings"
"testing"
)
type lexerTestCase struct {
input string
expected []token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
expected: []token{
{line: 1, text: "host:123"},
},
},
{
input: `host:123
directive`,
expected: []token{
{line: 1, text: "host:123"},
{line: 3, text: "directive"},
},
},
{
input: `host:123 {
directive
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 2, text: "directive"},
{line: 3, text: "}"},
},
},
{
input: `host:123 { directive }`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 1, text: "directive"},
{line: 1, text: "}"},
},
},
{
input: `host:123 {
#comment
directive
# comment
foobar # another comment
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 3, text: "directive"},
{line: 5, text: "foobar"},
{line: 6, text: "}"},
},
},
{
input: `a "quoted value" b
foobar`,
expected: []token{
{line: 1, text: "a"},
{line: 1, text: "quoted value"},
{line: 1, text: "b"},
{line: 2, text: "foobar"},
},
},
{
input: `A "quoted \"value\" inside" B`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: `quoted "value" inside`},
{line: 1, text: "B"},
},
},
{
input: `A "quoted value with line
break inside" {
foobar
}`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"},
{line: 2, text: "{"},
{line: 3, text: "foobar"},
{line: 4, text: "}"},
},
},
{
input: "skip those\r\nCR characters",
expected: []token{
{line: 1, text: "skip"},
{line: 1, text: "those"},
{line: 2, text: "CR"},
{line: 2, text: "characters"},
},
},
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
lexerCompare(t, i, testCase.expected, actual)
}
}
func tokenize(input string) (tokens []token) {
l := lexer{}
l.load(strings.NewReader(input))
for l.next() {
tokens = append(tokens, l.token)
}
return
}
func lexerCompare(t *testing.T, n int, expected, actual []token) {
if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
}
for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].line != expected[i].line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].text, expected[i].line, actual[i].line)
break
}
if actual[i].text != expected[i].text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].text, actual[i].text)
break
}
}
}

View File

@ -1,82 +0,0 @@
package config
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy/middleware/errors"
"github.com/mholt/caddy/middleware/extensions"
"github.com/mholt/caddy/middleware/fastcgi"
"github.com/mholt/caddy/middleware/git"
"github.com/mholt/caddy/middleware/gzip"
"github.com/mholt/caddy/middleware/headers"
"github.com/mholt/caddy/middleware/log"
"github.com/mholt/caddy/middleware/markdown"
"github.com/mholt/caddy/middleware/proxy"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/middleware/rewrite"
"github.com/mholt/caddy/middleware/templates"
"github.com/mholt/caddy/middleware/websockets"
)
// This init function registers middleware. Register
// middleware in the order they should be executed
// during a request (A, B, C...). Middleware execute
// in the order A-B-C-*-C-B-A, assuming they call
// the Next handler in the chain.
//
// Note: Ordering is VERY important. Every middleware
// will feel the effects of all other middleware below
// (after) them, but must not care what middleware above
// them are doing.
//
// For example, log needs to know the status code and exactly
// how many bytes were written to the client, which every
// other middleware can affect, so it gets registered first.
// The errors middleware does not care if gzip or log modifies
// its response, so it gets registered below them. Gzip, on the
// other hand, DOES care what errors does to the response since
// it must compress every output to the client, even error pages,
// so it must be registered before the errors middleware and any
// others that would write to the response.
func init() {
register("log", log.New)
register("gzip", gzip.New)
register("errors", errors.New)
register("header", headers.New)
register("rewrite", rewrite.New)
register("redir", redirect.New)
register("ext", extensions.New)
register("basicauth", basicauth.New)
register("proxy", proxy.New)
register("git", git.New)
register("fastcgi", fastcgi.New)
register("websocket", websockets.New)
register("markdown", markdown.New)
register("templates", templates.New)
register("browse", browse.New)
}
// registry stores the registered middleware:
// both the order and the directives to which they
// are bound.
var registry = struct {
directiveMap map[string]middleware.Generator
ordered []string
}{
directiveMap: make(map[string]middleware.Generator),
}
// 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 middleware.Generator) {
registry.directiveMap[directive] = generator
registry.ordered = append(registry.ordered, directive)
}
// middlewareRegistered returns whether or not a directive is registered.
func middlewareRegistered(directive string) bool {
_, ok := registry.directiveMap[directive]
return ok
}

View File

@ -1,208 +0,0 @@
package config
import (
"errors"
"fmt"
"os"
"github.com/mholt/caddy/middleware"
)
type (
// parser is a type which can parse config files.
parser struct {
filename string // the name of the file that we're parsing
lexer lexer // the lexer that is giving us tokens from the raw input
hosts []hostPort // the list of host:port combinations current tokens apply to
cfg Config // each virtual host gets one Config; this is the one we're currently building
cfgs []Config // after a Config is created, it may need to be copied for multiple hosts
other []locationContext // tokens to be 'parsed' later by middleware generators
scope *locationContext // the current location context (path scope) being populated
unused *token // sometimes a token will be read but not immediately consumed
eof bool // if we encounter a valid EOF in a hard place
}
// locationContext represents a location context
// (path block) in a config file. If no context
// is explicitly defined, the default location
// context is "/".
locationContext struct {
path string
directives map[string]*controller
}
// hostPort just keeps a hostname and port together
hostPort struct {
host, port string
}
)
// newParser makes a new parser and prepares it for parsing, given
// the input to parse.
func newParser(file *os.File) (*parser, error) {
stat, err := file.Stat()
if err != nil {
return nil, err
}
p := &parser{filename: stat.Name()}
p.lexer.load(file)
return p, nil
}
// 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) {
var configs []Config
for p.lexer.next() {
err := p.parseOne()
if err != nil {
return nil, err
}
configs = append(configs, p.cfgs...)
}
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 != nil {
return false
}
line, tkn := p.line(), p.lexer.token
if p.next() {
if p.line() > line {
p.unused = &tkn
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 != nil {
p.unused = nil
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 when you know that the lexer has another
// another token and you're not in another server
// block already.
func (p *parser) parseOne() error {
p.cfgs = []Config{}
p.cfg = Config{
Middleware: make(map[string][]middleware.Middleware),
}
p.other = []locationContext{}
err := p.begin()
if err != nil {
return err
}
err = p.unwrap()
if err != nil {
return err
}
// Make a copy of the config for each
// address that will be using it
for _, hostport := range p.hosts {
cfgCopy := p.cfg
cfgCopy.Host = hostport.host
cfgCopy.Port = hostport.port
p.cfgs = append(p.cfgs, cfgCopy)
}
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 are the actual middleware.
// This function should be called only after p has filled out
// p.other and the entire server block has already been consumed.
func (p *parser) unwrap() error {
if len(p.other) == 0 {
// no middlewares were invoked
return nil
}
for _, directive := range registry.ordered {
// TODO: For now, we only support the first and default path scope ("/", held in p.other[0])
// but when we implement support for path scopes, we will have to change this logic
// to loop over them and order them. We need to account for situations where multiple
// path scopes overlap, regex (??), etc...
if disp, ok := p.other[0].directives[directive]; ok {
if generator, ok := registry.directiveMap[directive]; ok {
mid, err := generator(disp)
if err != nil {
return err
}
if mid != nil {
// TODO: Again, we assume the default path scope here...
p.cfg.Middleware[p.other[0].path] = append(p.cfg.Middleware[p.other[0].path], mid)
}
} else {
return errors.New("No middleware bound to directive '" + directive + "'")
}
}
}
return nil
}
// tkn is shorthand to get the text/value of the current token.
func (p *parser) tkn() string {
if p.unused != nil {
return p.unused.text
}
return p.lexer.token.text
}
// line is shorthand to get the line number of the current token.
func (p *parser) line() int {
if p.unused != nil {
return p.unused.line
}
return p.lexer.token.line
}
// syntaxErr creates a syntax error which explains what was
// found and expected.
func (p *parser) syntaxErr(expected string) error {
return p.err("Syntax", fmt.Sprintf("Unexpected token '%s', expecting '%s'", p.tkn(), expected))
}
// syntaxErr creates a syntax error that explains that there
// weren't enough arguments on the line.
func (p *parser) argErr() error {
return p.err("Syntax", "Unexpected line break after '"+p.tkn()+"' (missing arguments?)")
}
// eofErr creates a syntax error describing an unexpected EOF.
func (p *parser) eofErr() error {
return p.err("Syntax", "Unexpected EOF")
}
// 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:%d - %s error: %s", p.filename, p.line(), kind, msg)
return errors.New(msg)
}

View File

@ -1,330 +0,0 @@
package config
import (
"os"
"strings"
"testing"
)
func TestNewParser(t *testing.T) {
filePath := "./parser_test.go"
expected := "parser_test.go"
file, err := os.Open(filePath)
if err != nil {
t.Fatal("Could not open file")
}
defer file.Close()
p, err := newParser(file)
if err != nil {
t.Fatal(err)
}
if p.filename != expected {
t.Errorf("Expected parser to have filename '%s' but had '%s'", expected, p.filename)
}
if p == nil {
t.Error("Expected parser to not be nil, but it was")
}
}
func TestParserBasic(t *testing.T) {
p := &parser{filename: "test"}
input := `localhost:1234
root /test/www
tls cert.pem key.pem`
p.lexer.load(strings.NewReader(input))
confs, err := p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
conf := confs[0]
if conf.Host != "localhost" {
t.Errorf("Expected host to be 'localhost', got '%s'", conf.Host)
}
if conf.Port != "1234" {
t.Errorf("Expected port to be '1234', got '%s'", conf.Port)
}
if conf.Root != "/test/www" {
t.Errorf("Expected root to be '/test/www', got '%s'", conf.Root)
}
if !conf.TLS.Enabled {
t.Error("Expected TLS to be enabled, but it wasn't")
}
if conf.TLS.Certificate != "cert.pem" {
t.Errorf("Expected TLS certificate to be 'cert.pem', got '%s'", conf.TLS.Certificate)
}
if conf.TLS.Key != "key.pem" {
t.Errorf("Expected TLS server key to be 'key.pem', got '%s'", conf.TLS.Key)
}
}
func TestParserBasicWithMultipleServerBlocks(t *testing.T) {
p := &parser{filename: "test"}
input := `host1.com:443 {
root /test/www
tls cert.pem key.pem
}
host2:80 {
root "/test/my site"
}`
p.lexer.load(strings.NewReader(input))
confs, err := p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
if len(confs) != 2 {
t.Fatalf("Expected 2 configurations, but got %d: %#v", len(confs), confs)
}
// First server
if confs[0].Host != "host1.com" {
t.Errorf("Expected first host to be 'host1.com', got '%s'", confs[0].Host)
}
if confs[0].Port != "443" {
t.Errorf("Expected first port to be '443', got '%s'", confs[0].Port)
}
if confs[0].Root != "/test/www" {
t.Errorf("Expected first root to be '/test/www', got '%s'", confs[0].Root)
}
if !confs[0].TLS.Enabled {
t.Error("Expected first TLS to be enabled, but it wasn't")
}
if confs[0].TLS.Certificate != "cert.pem" {
t.Errorf("Expected first TLS certificate to be 'cert.pem', got '%s'", confs[0].TLS.Certificate)
}
if confs[0].TLS.Key != "key.pem" {
t.Errorf("Expected first TLS server key to be 'key.pem', got '%s'", confs[0].TLS.Key)
}
// Second server
if confs[1].Host != "host2" {
t.Errorf("Expected second host to be 'host2', got '%s'", confs[1].Host)
}
if confs[1].Port != "80" {
t.Errorf("Expected second port to be '80', got '%s'", confs[1].Port)
}
if confs[1].Root != "/test/my site" {
t.Errorf("Expected second root to be '/test/my site', got '%s'", confs[1].Root)
}
if confs[1].TLS.Enabled {
t.Error("Expected second TLS to be disabled, but it was enabled")
}
if confs[1].TLS.Certificate != "" {
t.Errorf("Expected second TLS certificate to be '', got '%s'", confs[1].TLS.Certificate)
}
if confs[1].TLS.Key != "" {
t.Errorf("Expected second TLS server key to be '', got '%s'", confs[1].TLS.Key)
}
}
func TestParserBasicWithMultipleHostsPerBlock(t *testing.T) {
// This test is table-driven; it is expected that each
// input string produce the same set of configs.
for _, input := range []string{
`host1.com host2.com:1234
root /public_html`, // space-separated, no block
`host1.com, host2.com:1234
root /public_html`, // comma-separated, no block
`host1.com,
host2.com:1234
root /public_html`, // comma-separated, newlines, no block
`host1.com host2.com:1234 {
root /public_html
}`, // space-separated, block
`host1.com, host2.com:1234 {
root /public_html
}`, // comma-separated, block
`host1.com,
host2.com:1234 {
root /public_html
}`, // comma-separated, newlines, block
} {
p := &parser{filename: "test"}
p.lexer.load(strings.NewReader(input))
confs, err := p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
if len(confs) != 2 {
t.Fatalf("Expected 2 configurations, but got %d: %#v", len(confs), confs)
}
if confs[0].Host != "host1.com" {
t.Errorf("Expected host of first conf to be 'host1.com', got '%s'", confs[0].Host)
}
if confs[0].Port != DefaultPort {
t.Errorf("Expected port of first conf to be '%s', got '%s'", DefaultPort, confs[0].Port)
}
if confs[0].Root != "/public_html" {
t.Errorf("Expected root of first conf to be '/public_html', got '%s'", confs[0].Root)
}
if confs[1].Host != "host2.com" {
t.Errorf("Expected host of second conf to be 'host2.com', got '%s'", confs[1].Host)
}
if confs[1].Port != "1234" {
t.Errorf("Expected port of second conf to be '1234', got '%s'", confs[1].Port)
}
if confs[1].Root != "/public_html" {
t.Errorf("Expected root of second conf to be '/public_html', got '%s'", confs[1].Root)
}
}
}
func TestParserBasicWithAlternateAddressStyles(t *testing.T) {
p := &parser{filename: "test"}
input := `http://host1.com, https://host2.com,
host3.com:http, host4.com:1234 {
root /test/www
}`
p.lexer.load(strings.NewReader(input))
confs, err := p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
if len(confs) != 4 {
t.Fatalf("Expected 4 configurations, but got %d: %#v", len(confs), confs)
}
for _, conf := range confs {
if conf.Root != "/test/www" {
t.Fatalf("Expected root for conf of %s to be '/test/www', but got: %s", conf.Address(), conf.Root)
}
}
p = &parser{filename: "test"}
input = `host:port, http://host:port, http://host, https://host:port, host`
p.lexer.load(strings.NewReader(input))
confs, err = p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
if len(confs) != 5 {
t.Fatalf("Expected 5 configurations, but got %d: %#v", len(confs), confs)
}
if confs[0].Host != "host" {
t.Errorf("Expected conf[0] Host='host', got '%#v'", confs[0])
}
if confs[0].Port != "port" {
t.Errorf("Expected conf[0] Port='port', got '%#v'", confs[0])
}
if confs[1].Host != "host" {
t.Errorf("Expected conf[1] Host='host', got '%#v'", confs[1])
}
if confs[1].Port != "port" {
t.Errorf("Expected conf[1] Port='port', got '%#v'", confs[1])
}
if confs[2].Host != "host" {
t.Errorf("Expected conf[2] Host='host', got '%#v'", confs[2])
}
if confs[2].Port != "http" {
t.Errorf("Expected conf[2] Port='http', got '%#v'", confs[2])
}
if confs[3].Host != "host" {
t.Errorf("Expected conf[3] Host='host', got '%#v'", confs[3])
}
if confs[3].Port != "port" {
t.Errorf("Expected conf[3] Port='port', got '%#v'", confs[3])
}
if confs[4].Host != "host" {
t.Errorf("Expected conf[4] Host='host', got '%#v'", confs[4])
}
if confs[4].Port != DefaultPort {
t.Errorf("Expected conf[4] Port='%s', got '%#v'", DefaultPort, confs[4].Port)
}
}
func TestParserImport(t *testing.T) {
p := &parser{filename: "test"}
input := `host:123
import import_test.txt`
p.lexer.load(strings.NewReader(input))
confs, err := p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
conf := confs[0]
if conf.Host != "host" {
t.Errorf("Expected host to be 'host', got '%s'", conf.Host)
}
if conf.Port != "123" {
t.Errorf("Expected port to be '123', got '%s'", conf.Port)
}
if conf.Root != "/test/imported/public_html" {
t.Errorf("Expected root to be '/test/imported/public_html', got '%s'", conf.Root)
}
if conf.TLS.Enabled {
t.Error("Expected TLS to be disabled, but it was enabled")
}
if conf.TLS.Certificate != "" {
t.Errorf("Expected TLS certificate to be '', got '%s'", conf.TLS.Certificate)
}
if conf.TLS.Key != "" {
t.Errorf("Expected TLS server key to be '', got '%s'", conf.TLS.Key)
}
}
func TestParserLocationContext(t *testing.T) {
p := &parser{filename: "test"}
input := `host:123 {
/scope {
gzip
}
}`
p.lexer.load(strings.NewReader(input))
confs, err := p.parse()
if err != nil {
t.Fatalf("Expected no errors, but got '%s'", err)
}
if len(confs) != 1 {
t.Fatalf("Expected 1 configuration, but got %d: %#v", len(confs), confs)
}
if len(p.other) != 2 {
t.Fatalf("Expected 2 path scopes, but got %d: %#v", len(p.other), p.other)
}
if p.other[0].path != "/" {
t.Fatalf("Expected first path scope to be default '/', but got %v: %#v", p.other[0].path, p.other)
}
if p.other[1].path != "/scope" {
t.Fatalf("Expected first path scope to be '/scope', but got %v: %#v", p.other[0].path, p.other)
}
if dir, ok := p.other[1].directives["gzip"]; !ok {
t.Fatalf("Expected scoped directive to be gzip, but got %v: %#v", dir, p.other[1].directives)
}
}

View File

@ -1,318 +0,0 @@
package config
import (
"errors"
"net"
"strings"
)
// This file contains the recursive-descent parsing
// functions.
// begin is the top of the recursive-descent parsing.
// It parses at most one server configuration (an address
// and its directives).
func (p *parser) begin() error {
err := p.addresses()
if err != nil {
return err
}
err = p.addressBlock()
if err != nil {
return err
}
return nil
}
// addresses expects that the current token is a
// "scheme://host:port" combination (the "scheme://"
// and/or ":port" portions may be omitted). If multiple
// addresses are specified, they must be space-
// separated on the same line, or each token must end
// with a comma.
func (p *parser) addresses() error {
var expectingAnother bool
p.hosts = []hostPort{}
// address gets host and port in a format accepted by net.Dial
address := func(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
}
for {
tkn, startLine := p.tkn(), p.line()
// Open brace definitely indicates end of addresses
if tkn == "{" {
if expectingAnother {
return p.err("Syntax", "Expected another address but had '"+tkn+"' - check for extra comma")
}
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 := address(tkn)
if err != nil {
return err
}
p.hosts = append(p.hosts, hostPort{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
}
// 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. It is expected
// that the current token is already the beginning of
// the address block.
func (p *parser) addressBlock() error {
errOpenCurlyBrace := p.openCurlyBrace()
if errOpenCurlyBrace != nil {
// meh, single-server configs don't need curly braces
// but we read a token and we won't consume it; mark it unused
p.unused = &p.lexer.token
}
// When we enter an address block, we also implicitly
// enter a path block where the path is all paths ("/")
p.other = append(p.other, locationContext{
path: "/",
directives: make(map[string]*controller),
})
p.scope = &p.other[0]
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
}
// 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.tkn() != "{" {
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.tkn() != "}" {
return p.syntaxErr("}")
}
return nil
}
// directives parses through all the directives
// and it expects the current token to be the first
// directive. It goes until EOF or closing curly
// brace which ends the address block.
func (p *parser) directives() error {
for p.next() {
if p.tkn() == "}" {
// end of address scope
break
}
if p.tkn()[0] == '/' || p.tkn()[0] == '*' {
// Path scope (a.k.a. location context)
// Starts with / ('starts with') or * ('ends with').
// TODO: The parser can handle the syntax (obviously), but the
// implementation is incomplete. This is intentional,
// until we can better decide what kind of feature set we
// want to support and how exactly we want these location
// scopes to work. Until this is ready, we leave this
// syntax undocumented. Some changes will need to be
// made in parser.go also (the unwrap function) and
// probably in server.go when we do this... see those TODOs.
var scope *locationContext
// If the path block is a duplicate, append to existing one
for i := 0; i < len(p.other); i++ {
if p.other[i].path == p.tkn() {
scope = &p.other[i]
break
}
}
// Otherwise, for a new path we haven't seen before, create a new context
if scope == nil {
scope = &locationContext{
path: p.tkn(),
directives: make(map[string]*controller),
}
}
// Consume the opening curly brace
if !p.next() {
return p.eofErr()
}
err := p.openCurlyBrace()
if err != nil {
return err
}
// Use this path scope as our current context for just a moment
p.scope = scope
// Consume each directive in the path block
for p.next() {
err := p.closeCurlyBrace()
if err == nil {
break
}
err = p.directive()
if err != nil {
return err
}
}
// Save the new scope and put the current scope back to "/"
p.other = append(p.other, *scope)
p.scope = &p.other[0]
} else if err := p.directive(); err != nil {
return err
}
}
return nil
}
// directive asserts that the current token is either a built-in
// directive or a registered middleware directive; otherwise an error
// will be returned. If it is a valid directive, tokens will be
// collected.
func (p *parser) directive() error {
if fn, ok := validDirectives[p.tkn()]; ok {
// Built-in (standard, or 'core') directive
err := fn(p)
if err != nil {
return err
}
} else if middlewareRegistered(p.tkn()) {
// Middleware directive
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).
// It creates a controller which is stored in the parser for
// later use by the middleware.
func (p *parser) collectTokens() error {
if p.scope == nil {
return errors.New("Current scope cannot be nil")
}
directive := p.tkn()
line := p.line()
nesting := 0
cont := newController(p)
// Re-use a duplicate directive's controller 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.scope.directives[directive]; ok {
cont = existing
}
// The directive is appended as a relevant token
cont.tokens = append(cont.tokens, p.lexer.token)
for p.next() {
if p.tkn() == "{" {
nesting++
} else if p.line() > line && nesting == 0 {
p.unused = &p.lexer.token
break
} else if p.tkn() == "}" && nesting > 0 {
nesting--
} else if p.tkn() == "}" && nesting == 0 {
return p.err("Syntax", "Unexpected '}' because no matching opening brace")
}
cont.tokens = append(cont.tokens, p.lexer.token)
}
if nesting > 0 {
return p.eofErr()
}
p.scope.directives[directive] = cont
return nil
}

View File

@ -1 +0,0 @@
package config

56
main.go
View File

@ -6,6 +6,8 @@ import (
"fmt"
"log"
"net"
"os"
"path"
"runtime"
"strconv"
"strings"
@ -23,10 +25,11 @@ var (
)
func init() {
flag.StringVar(&conf, "conf", config.DefaultConfigFile, "the configuration file to use")
flag.BoolVar(&http2, "http2", true, "enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.BoolVar(&quiet, "quiet", false, "quiet mode (no initialization output)")
flag.StringVar(&conf, "conf", config.DefaultConfigFile, "The configuration file to use")
flag.BoolVar(&http2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.BoolVar(&quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site")
flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host")
flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port")
flag.Parse()
@ -42,16 +45,9 @@ func main() {
}
// Load config from file
allConfigs, err := config.Load(conf)
allConfigs, err := loadConfigs(conf)
if err != nil {
if config.IsNotFound(err) {
allConfigs = config.Default()
} else {
log.Fatal(err)
}
}
if len(allConfigs) == 0 {
allConfigs = config.Default()
log.Fatal(err)
}
// Group by address (virtual hosts)
@ -86,13 +82,45 @@ func main() {
wg.Wait()
}
// loadConfigs loads configuration from a file.
func loadConfigs(confPath string) ([]server.Config, error) {
var allConfigs []server.Config
file, err := os.Open(confPath)
if err == nil {
defer file.Close()
allConfigs, err = config.Load(path.Base(confPath), file)
if err != nil {
return allConfigs, err
}
} else {
if os.IsNotExist(err) {
// This is only a problem if the user
// explicitly specified a config file
if confPath != config.DefaultConfigFile {
return allConfigs, err
}
} else {
// ... but anything else is always a problem
return allConfigs, err
}
}
// If config file was empty or didn't exist, use default
if len(allConfigs) == 0 {
allConfigs = []server.Config{config.Default()}
}
return allConfigs, nil
}
// arrangeBindings groups configurations by their bind address. For example,
// a server that should listen on localhost and another on 127.0.0.1 will
// be grouped into the same address: 127.0.0.1. It will return an error
// if the address lookup fails or if a TLS listener is configured on the
// same address as a plaintext HTTP listener.
func arrangeBindings(allConfigs []config.Config) (map[string][]config.Config, error) {
addresses := make(map[string][]config.Config)
func arrangeBindings(allConfigs []server.Config) (map[string][]server.Config, error) {
addresses := make(map[string][]server.Config)
// Group configs by bind address
for _, conf := range allConfigs {

View File

@ -7,21 +7,14 @@ import (
"github.com/mholt/caddy/middleware"
)
// New constructs a new BasicAuth middleware instance.
func New(c middleware.Controller) (middleware.Middleware, error) {
rules, err := parse(c)
if err != nil {
return nil, err
}
basic := BasicAuth{
Rules: rules,
}
return func(next middleware.Handler) middleware.Handler {
basic.Next = next
return basic
}, nil
// BasicAuth is middleware to protect resources with a username and password.
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next middleware.Handler
Rules []Rule
}
// ServeHTTP implements the middleware.Handler interface.
@ -50,48 +43,6 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
return a.Next.ServeHTTP(w, r)
}
func parse(c middleware.Controller) ([]Rule, error) {
var rules []Rule
for c.Next() {
var rule 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.Err("Expecting only one resource per line (extra '" + 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
}
// BasicAuth is middleware to protect resources with a username and password.
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next middleware.Handler
Rules []Rule
}
// Rule represents a BasicAuth rule. A username and password
// combination protect the associated resources, which are
// file or directory paths.

View File

@ -7,45 +7,10 @@ import (
"log"
"net/http"
"os"
"path"
"strconv"
"github.com/mholt/caddy/middleware"
)
// New instantiates a new instance of error-handling middleware.
func New(c middleware.Controller) (middleware.Middleware, error) {
handler, err := parse(c)
if err != nil {
return nil, err
}
// Open the log file for writing when the server starts
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
}
// ErrorHandler handles HTTP errors (or errors from other middleware).
type ErrorHandler struct {
Next middleware.Handler
@ -113,63 +78,4 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, code int) {
http.Error(w, defaultBody, code)
}
func parse(c middleware.Controller) (*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 := &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 = defaultLogFilename
}
}
}
return handler, nil
}
const defaultLogFilename = "error.log"
const DefaultLogFilename = "error.log"

View File

@ -20,13 +20,6 @@ type Gzip struct {
Next middleware.Handler
}
// New creates a new gzip middleware instance.
func New(c middleware.Controller) (middleware.Middleware, error) {
return func(next middleware.Handler) middleware.Handler {
return Gzip{Next: next}
}, nil
}
// ServeHTTP serves a gzipped response if the client supports it.
func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {

View File

@ -13,7 +13,7 @@ import (
// for requests matching a certain path.
type Headers struct {
Next middleware.Handler
Rules []HeaderRule
Rules []Rule
}
// ServeHTTP implements the middleware.Handler interface and serves requests,
@ -30,9 +30,9 @@ func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
}
type (
// HeaderRule groups a slice of HTTP headers by a URL pattern.
// Rule groups a slice of HTTP headers by a URL pattern.
// TODO: use http.Header type instead?
HeaderRule struct {
Rule struct {
Url string
Headers []Header
}

View File

@ -1,16 +0,0 @@
package headers
import "github.com/mholt/caddy/middleware"
// New constructs and configures a new headers middleware instance.
func New(c middleware.Controller) (middleware.Middleware, error) {
rules, err := parse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return Headers{Next: next, Rules: rules}
}, nil
}

View File

@ -1,69 +0,0 @@
package headers
import "github.com/mholt/caddy/middleware"
func parse(c middleware.Controller) ([]HeaderRule, error) {
var rules []HeaderRule
for c.NextLine() {
var head HeaderRule
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 := 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 := 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
}

View File

@ -4,44 +4,13 @@ package log
import (
"log"
"net/http"
"os"
"github.com/mholt/caddy/middleware"
)
// New instantiates a new instance of logging middleware.
func New(c middleware.Controller) (middleware.Middleware, error) {
rules, err := parse(c)
if err != nil {
return nil, err
}
// Open the log files for writing when the server starts
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 Logger{Next: next, Rules: rules}
}, nil
type Logger struct {
Next middleware.Handler
Rules []LogRule
}
func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
@ -57,58 +26,6 @@ func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
return l.Next.ServeHTTP(w, r)
}
func parse(c middleware.Controller) ([]LogRule, error) {
var rules []LogRule
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
// Nothing specified; use defaults
rules = append(rules, LogRule{
PathScope: "/",
OutputFile: defaultLogFilename,
Format: defaultLogFormat,
})
} else if len(args) == 1 {
// Only an output file specified
rules = append(rules, LogRule{
PathScope: "/",
OutputFile: args[0],
Format: defaultLogFormat,
})
} else {
// Path scope, output file, and maybe a format specified
format := defaultLogFormat
if len(args) > 2 {
switch args[2] {
case "{common}":
format = commonLogFormat
case "{combined}":
format = combinedLogFormat
default:
format = args[2]
}
}
rules = append(rules, LogRule{
PathScope: args[0],
OutputFile: args[1],
Format: format,
})
}
}
return rules, nil
}
type Logger struct {
Next middleware.Handler
Rules []LogRule
}
type LogRule struct {
PathScope string
OutputFile string
@ -117,8 +34,8 @@ type LogRule struct {
}
const (
defaultLogFilename = "access.log"
commonLogFormat = `{remote} ` + middleware.EmptyStringReplacer + ` [{when}] "{method} {uri} {proto}" {status} {size}`
combinedLogFormat = commonLogFormat + ` "{>Referer}" "{>User-Agent}"`
defaultLogFormat = commonLogFormat
DefaultLogFilename = "access.log"
CommonLogFormat = `{remote} ` + middleware.EmptyStringReplacer + ` [{when}] "{method} {uri} {proto}" {status} {size}`
CombinedLogFormat = CommonLogFormat + ` "{>Referer}" "{>User-Agent}"`
DefaultLogFormat = CommonLogFormat
)

View File

@ -9,18 +9,6 @@ import (
"github.com/mholt/caddy/middleware"
)
// New instantiates a new Redirect middleware.
func New(c middleware.Controller) (middleware.Middleware, error) {
rules, err := parse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return Redirect{Next: next, Rules: rules}
}, nil
}
// Redirect is middleware to respond with HTTP redirects
type Redirect struct {
Next middleware.Handler
@ -43,65 +31,8 @@ func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
return rd.Next.ServeHTTP(w, r)
}
func parse(c middleware.Controller) ([]Rule, error) {
var redirects []Rule
for c.Next() {
var rule 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
}
// Rule describes an HTTP redirect rule.
type Rule struct {
From, To string
Code int
}
// 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,
}

View File

@ -8,22 +8,10 @@ import (
"github.com/mholt/caddy/middleware"
)
// New instantiates a new Rewrites middleware.
func New(c middleware.Controller) (middleware.Middleware, error) {
rewrites, err := parse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return Rewrite{Next: next, Rules: rewrites}
}, nil
}
// Rewrite is middleware to rewrite request locations internally before being handled.
type Rewrite struct {
Next middleware.Handler
Rules []RewriteRule
Rules []Rule
}
// ServeHTTP implements the middleware.Handler interface.
@ -37,29 +25,7 @@ func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
return rw.Next.ServeHTTP(w, r)
}
func parse(c middleware.Controller) ([]RewriteRule, error) {
var rewrites []RewriteRule
for c.Next() {
var rule RewriteRule
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
}
// RewriteRule describes an internal location rewrite rule.
type RewriteRule struct {
// A Rule describes an internal location rewrite rule.
type Rule struct {
From, To string
}

View File

@ -13,7 +13,6 @@ import (
"os/signal"
"github.com/bradfitz/http2"
"github.com/mholt/caddy/config"
)
// Server represents an instance of a server, which serves
@ -28,7 +27,7 @@ type Server struct {
// New creates a new Server which will bind to addr and serve
// the sites/hosts configured in configs. This function does
// not start serving.
func New(addr string, configs []config.Config, tls bool) (*Server, error) {
func New(addr string, configs []Config, tls bool) (*Server, error) {
s := &Server{
address: addr,
tls: tls,
@ -93,7 +92,7 @@ func (s *Server) Serve() error {
}
if s.tls {
var tlsConfigs []config.TLSConfig
var tlsConfigs []TLSConfig
for _, vh := range s.vhosts {
tlsConfigs = append(tlsConfigs, vh.config.TLS)
}
@ -107,7 +106,7 @@ func (s *Server) Serve() error {
// multiple sites (different hostnames) to be served from the same address. This method is
// adapted directly from the std lib's net/http ListenAndServeTLS function, which was
// written by the Go Authors. It has been modified to support multiple certificate/key pairs.
func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []config.TLSConfig) error {
func ListenAndServeTLSWithSNI(srv *http.Server, tlsConfigs []TLSConfig) error {
addr := srv.Addr
if addr == "" {
addr = ":https"

View File

@ -3,7 +3,6 @@ package server
import (
"net/http"
"github.com/mholt/caddy/config"
"github.com/mholt/caddy/middleware"
)
@ -12,7 +11,7 @@ import (
// multiple sites on a single address, and this is what a
// virtualHost allows us to do.
type virtualHost struct {
config config.Config
config Config
fileServer middleware.Handler
stack middleware.Handler
}