diff options
Diffstat (limited to 'config')
-rw-r--r-- | config/config.go | 153 | ||||
-rw-r--r-- | config/controller.go | 48 | ||||
-rw-r--r-- | config/controller_test.go | 37 | ||||
-rw-r--r-- | config/directives.go | 164 | ||||
-rw-r--r-- | config/dispenser.go | 159 | ||||
-rw-r--r-- | config/dispenser_test.go | 310 | ||||
-rw-r--r-- | config/import_test.txt | 1 | ||||
-rw-r--r-- | config/lexer.go | 114 | ||||
-rw-r--r-- | config/lexer_test.go | 139 | ||||
-rw-r--r-- | config/middleware.go | 82 | ||||
-rw-r--r-- | config/parser.go | 208 | ||||
-rw-r--r-- | config/parser_test.go | 330 | ||||
-rw-r--r-- | config/parsing.go | 318 | ||||
-rw-r--r-- | config/parsing_test.go | 1 |
14 files changed, 101 insertions, 1963 deletions
diff --git a/config/config.go b/config/config.go index f069f4f05..bde7854eb 100644 --- a/config/config.go +++ b/config/config.go @@ -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 - } - - for i := 0; i < len(cfgs); i++ { - cfgs[i].ConfigFile = filename + // 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), + } + + // 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) +// 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 -// 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, - }, +// 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 } diff --git a/config/controller.go b/config/controller.go deleted file mode 100644 index 1ce6db7f5..000000000 --- a/config/controller.go +++ /dev/null @@ -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) -} diff --git a/config/controller_test.go b/config/controller_test.go deleted file mode 100644 index 7dccf0f14..000000000 --- a/config/controller_test.go +++ /dev/null @@ -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) - } -} diff --git a/config/directives.go b/config/directives.go index e39eae235..76f29af73 100644 --- a/config/directives.go +++ b/config/directives.go @@ -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()) - } + // 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{}{} + } +} - 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 - } +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}, +} - p.cfg.Shutdown = append(p.cfg.Shutdown, shutdownfn) - return nil - }, - } +type directive struct { + name string + setup setupFunc } + +type setupFunc func(c *setup.Controller) (middleware.Middleware, error) diff --git a/config/dispenser.go b/config/dispenser.go deleted file mode 100644 index 13cd85425..000000000 --- a/config/dispenser.go +++ /dev/null @@ -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) -} diff --git a/config/dispenser_test.go b/config/dispenser_test.go deleted file mode 100644 index 2940feb1c..000000000 --- a/config/dispenser_test.go +++ /dev/null @@ -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 -} diff --git a/config/import_test.txt b/config/import_test.txt deleted file mode 100644 index 78cb08282..000000000 --- a/config/import_test.txt +++ /dev/null @@ -1 +0,0 @@ -root /test/imported/public_html
\ No newline at end of file diff --git a/config/lexer.go b/config/lexer.go deleted file mode 100644 index 96c883cff..000000000 --- a/config/lexer.go +++ /dev/null @@ -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) - } -} diff --git a/config/lexer_test.go b/config/lexer_test.go deleted file mode 100644 index 5b1cd2c25..000000000 --- a/config/lexer_test.go +++ /dev/null @@ -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 - } - } -} diff --git a/config/middleware.go b/config/middleware.go deleted file mode 100644 index 41af1826d..000000000 --- a/config/middleware.go +++ /dev/null @@ -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 -} diff --git a/config/parser.go b/config/parser.go deleted file mode 100644 index 1c8cc2c87..000000000 --- a/config/parser.go +++ /dev/null @@ -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) -} diff --git a/config/parser_test.go b/config/parser_test.go deleted file mode 100644 index 7502c317e..000000000 --- a/config/parser_test.go +++ /dev/null @@ -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) - } -} diff --git a/config/parsing.go b/config/parsing.go deleted file mode 100644 index 0da600b30..000000000 --- a/config/parsing.go +++ /dev/null @@ -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 -} diff --git a/config/parsing_test.go b/config/parsing_test.go deleted file mode 100644 index d912156be..000000000 --- a/config/parsing_test.go +++ /dev/null @@ -1 +0,0 @@ -package config |