mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	Bringing in latest from master; refactoring under way
This commit is contained in:
		
							parent
							
								
									5f32f9b1c8
								
							
						
					
					
						commit
						995edf0566
					
				
							
								
								
									
										155
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								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 | ||||
| 	} | ||||
| 	// 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 | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
| @ -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) | ||||
| 	} | ||||
| } | ||||
| @ -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) | ||||
|  | ||||
| @ -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) | ||||
| } | ||||
| @ -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 | ||||
| } | ||||
| @ -1 +0,0 @@ | ||||
| root /test/imported/public_html | ||||
							
								
								
									
										114
									
								
								config/lexer.go
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								config/lexer.go
									
									
									
									
									
								
							| @ -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) | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
| } | ||||
							
								
								
									
										208
									
								
								config/parser.go
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								config/parser.go
									
									
									
									
									
								
							| @ -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) | ||||
| } | ||||
| @ -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) | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
| } | ||||
| @ -1 +0,0 @@ | ||||
| package config | ||||
							
								
								
									
										56
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								main.go
									
									
									
									
									
								
							| @ -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 { | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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") { | ||||
|  | ||||
| @ -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 | ||||
| 	} | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
| @ -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 | ||||
| } | ||||
| @ -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 | ||||
| ) | ||||
|  | ||||
| @ -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, | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user