diff --git a/README.md b/README.md index 44fd023b..917e094c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com) -[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square)](https://travis-ci.org/mholt/caddy) +[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Linux Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square&label=linux+build)](https://travis-ci.org/mholt/caddy) [![Windows Build Status](https://img.shields.io/appveyor/ci/mholt/caddy.svg?style=flat-square&label=windows+build)](https://ci.appveyor.com/project/mholt/caddy) Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is a capable alternative to other popular and easy to use web servers. diff --git a/app/app.go b/app/app.go index e8cd16bd..c63cc833 100644 --- a/app/app.go +++ b/app/app.go @@ -22,7 +22,7 @@ const ( Name = "Caddy" // Version is the program version - Version = "0.7.5" + Version = "0.7.6" ) var ( @@ -35,8 +35,8 @@ var ( // Wg is used to wait for all servers to shut down Wg sync.WaitGroup - // Http2 indicates whether HTTP2 is enabled or not - Http2 bool // TODO: temporary flag until http2 is standard + // HTTP2 indicates whether HTTP2 is enabled or not + HTTP2 bool // TODO: temporary flag until http2 is standard // Quiet mode hides non-error initialization output Quiet bool diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..a436177c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,20 @@ +version: "{build}" + +os: Windows Server 2012 R2 + +clone_folder: c:\gopath\src\github.com\mholt\caddy + +environment: + GOPATH: c:\gopath + +install: + - go get golang.org/x/tools/cmd/vet + - echo %PATH% + - echo %GOPATH% + - go version + - go env + - go get -d ./... + +build_script: + - go vet ./... + - go test ./... \ No newline at end of file diff --git a/config/config.go b/config/config.go index ea92368e..fee6b34e 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "io" "log" "net" + "sync" "github.com/mholt/caddy/app" "github.com/mholt/caddy/config/parse" @@ -42,32 +43,59 @@ func Load(filename string, input io.Reader) (Group, error) { return Default() } - // Each server block represents one or more servers/addresses. + // Each server block represents similar hosts/addresses. // Iterate each server block and make a config for each one, // executing the directives that were parsed. - for _, sb := range serverBlocks { - sharedConfig, err := serverBlockToConfig(filename, sb) - if err != nil { - return nil, err - } + for i, sb := range serverBlocks { + onces := makeOnces() + + for _, addr := range sb.Addresses { + config := server.Config{ + Host: addr.Host, + Port: addr.Port, + Root: Root, + Middleware: make(map[string][]middleware.Middleware), + ConfigFile: filename, + AppName: app.Name, + AppVersion: app.Version, + } + + // 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), + OncePerServerBlock: func(f func() error) error { + var err error + onces[dir.name].Do(func() { + err = f() + }) + return err + }, + ServerBlockIndex: i, + ServerBlockHosts: sb.HostList(), + } + + midware, err := dir.setup(controller) + if err != nil { + return nil, err + } + if midware != nil { + // TODO: For now, we only support the default path scope / + config.Middleware["/"] = append(config.Middleware["/"], midware) + } + } + } - // Now share the config with as many hosts as share the server block - for i, addr := range sb.Addresses { - config := sharedConfig - config.Host = addr.Host - config.Port = addr.Port if config.Port == "" { config.Port = Port } - if config.Port == "http" { - config.TLS.Enabled = false - log.Printf("Warning: TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+ - "specify port 80 explicitly (https://%s:80).", config.Port, config.Host, config.Host) - } - if i == 0 { - sharedConfig.Startup = []func() error{} - sharedConfig.Shutdown = []func() error{} - } + configs = append(configs, config) } } @@ -106,44 +134,21 @@ func Load(filename string, input io.Reader) (Group, error) { return arrangeBindings(configs) } -// serverBlockToConfig makes a config for the server block -// by executing the tokens that were parsed. The returned -// config is shared among all hosts/addresses for the server -// block, so Host and Port information is not filled out -// here. -func serverBlockToConfig(filename string, sb parse.ServerBlock) (server.Config, error) { - sharedConfig := server.Config{ - Root: Root, - Middleware: make(map[string][]middleware.Middleware), - ConfigFile: filename, - AppName: app.Name, - AppVersion: app.Version, - } - - // It is crucial that directives are executed in the proper order. +// makeOnces makes a map of directive name to sync.Once +// instance. This is intended to be called once per server +// block when setting up configs so that Setup functions +// for each directive can perform a task just once per +// server block, even if there are multiple hosts on the block. +// +// We need one Once per directive, otherwise the first +// directive to use it would exclude other directives from +// using it at all, which would be a bug. +func makeOnces() map[string]*sync.Once { + onces := make(map[string]*sync.Once) 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: &sharedConfig, - Dispenser: parse.NewDispenserTokens(filename, tokens), - } - - midware, err := dir.setup(controller) - if err != nil { - return sharedConfig, err - } - if midware != nil { - // TODO: For now, we only support the default path scope / - sharedConfig.Middleware["/"] = append(sharedConfig.Middleware["/"], midware) - } - } + onces[dir.name] = new(sync.Once) } - - return sharedConfig, nil + return onces } // arrangeBindings groups configurations by their bind address. For example, @@ -154,8 +159,8 @@ func serverBlockToConfig(filename string, sb parse.ServerBlock) (server.Config, // bind address to list of configs that would become VirtualHosts on that // server. Use the keys of the returned map to create listeners, and use // the associated values to set up the virtualhosts. -func arrangeBindings(allConfigs []server.Config) (Group, error) { - addresses := make(Group) +func arrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Config, error) { + addresses := make(map[*net.TCPAddr][]server.Config) // Group configs by bind address for _, conf := range allConfigs { @@ -263,6 +268,9 @@ func validDirective(d string) bool { return false } +// NewDefault makes a default configuration, which +// is empty except for root, host, and port, +// which are essentials for serving the cwd. func NewDefault() server.Config { return server.Config{ Root: Root, @@ -271,9 +279,8 @@ func NewDefault() server.Config { } } -// Default makes a default configuration which -// is empty except for root, host, and port, -// which are essentials for serving the cwd. +// Default obtains a default config and arranges +// bindings so it's ready to use. func Default() (Group, error) { return arrangeBindings([]server.Config{NewDefault()}) } @@ -296,4 +303,5 @@ var ( LetsEncryptAgree bool ) +// Group maps network addresses to their configurations. type Group map[*net.TCPAddr][]server.Config diff --git a/config/directives.go b/config/directives.go index 6a9124d6..354b5595 100644 --- a/config/directives.go +++ b/config/directives.go @@ -57,6 +57,7 @@ var directiveOrder = []directive{ {"rewrite", setup.Rewrite}, {"redir", setup.Redir}, {"ext", setup.Ext}, + {"mime", setup.Mime}, {"basicauth", setup.BasicAuth}, {"internal", setup.Internal}, {"proxy", setup.Proxy}, @@ -73,7 +74,7 @@ type directive struct { setup SetupFunc } -// A setup function takes a setup controller. Its return values may -// both be nil. If middleware is not nil, it will be chained into +// SetupFunc takes a controller and may optionally return a middleware. +// If the resulting middleware is not nil, it will be chained into // the HTTP handlers in the order specified in this package. type SetupFunc func(c *setup.Controller) (middleware.Middleware, error) diff --git a/config/parse/dispenser.go b/config/parse/dispenser.go index a7457f56..08aa6e76 100644 --- a/config/parse/dispenser.go +++ b/config/parse/dispenser.go @@ -119,6 +119,7 @@ func (d *Dispenser) NextBlock() bool { return true } +// IncrNest adds a level of nesting to the dispenser. func (d *Dispenser) IncrNest() { d.nesting++ return @@ -208,9 +209,9 @@ func (d *Dispenser) SyntaxErr(expected string) error { return errors.New(msg) } -// EofErr returns an EOF error, meaning that end of input -// was found when another token was expected. -func (d *Dispenser) EofErr() error { +// EOFErr returns an error indicating that the dispenser reached +// the end of the input when searching for the next token. +func (d *Dispenser) EOFErr() error { return d.Errf("Unexpected EOF") } diff --git a/config/parse/parse.go b/config/parse/parse.go index dbb62a36..b44041d4 100644 --- a/config/parse/parse.go +++ b/config/parse/parse.go @@ -6,7 +6,7 @@ import "io" // ServerBlocks parses the input just enough to organize tokens, // in order, by server block. No further parsing is performed. // Server blocks are returned in the order in which they appear. -func ServerBlocks(filename string, input io.Reader) ([]ServerBlock, error) { +func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) { p := parser{Dispenser: NewDispenser(filename, input)} blocks, err := p.parseAll() return blocks, err diff --git a/config/parse/parsing.go b/config/parse/parsing.go index bd903503..59455391 100644 --- a/config/parse/parsing.go +++ b/config/parse/parsing.go @@ -9,12 +9,12 @@ import ( type parser struct { Dispenser - block ServerBlock // current server block being parsed + block serverBlock // current server block being parsed eof bool // if we encounter a valid EOF in a hard place } -func (p *parser) parseAll() ([]ServerBlock, error) { - var blocks []ServerBlock +func (p *parser) parseAll() ([]serverBlock, error) { + var blocks []serverBlock for p.Next() { err := p.parseOne() @@ -30,7 +30,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) { } func (p *parser) parseOne() error { - p.block = ServerBlock{Tokens: make(map[string][]token)} + p.block = serverBlock{Tokens: make(map[string][]token)} err := p.begin() if err != nil { @@ -87,7 +87,7 @@ func (p *parser) addresses() error { break } - if tkn != "" { + if tkn != "" { // empty token possible if user typed "" in Caddyfile // Trailing comma indicates another address will follow, which // may possibly be on the next line if tkn[len(tkn)-1] == ',' { @@ -102,13 +102,13 @@ func (p *parser) addresses() error { if err != nil { return err } - p.block.Addresses = append(p.block.Addresses, Address{host, port}) + p.block.Addresses = append(p.block.Addresses, address{host, port}) } // Advance token and possibly break out of loop or return error hasNext := p.Next() if expectingAnother && !hasNext { - return p.EofErr() + return p.EOFErr() } if !hasNext { p.eof = true @@ -242,7 +242,7 @@ func (p *parser) directive() error { } if nesting > 0 { - return p.EofErr() + return p.EOFErr() } return nil } @@ -301,15 +301,26 @@ func standardAddress(str string) (host, port string, err error) { } type ( - // ServerBlock associates tokens with a list of addresses + // serverBlock associates tokens with a list of addresses // and groups tokens by directive name. - ServerBlock struct { - Addresses []Address + serverBlock struct { + Addresses []address Tokens map[string][]token } - // Address represents a host and port. - Address struct { + address struct { Host, Port string } ) + +// HostList converts the list of addresses (hosts) +// that are associated with this server block into +// a slice of strings. Each string is a host:port +// combination. +func (sb serverBlock) HostList() []string { + sbHosts := make([]string, len(sb.Addresses)) + for j, addr := range sb.Addresses { + sbHosts[j] = net.JoinHostPort(addr.Host, addr.Port) + } + return sbHosts +} diff --git a/config/parse/parsing_test.go b/config/parse/parsing_test.go index 8fa55fd6..c8a7ef0b 100644 --- a/config/parse/parsing_test.go +++ b/config/parse/parsing_test.go @@ -59,7 +59,7 @@ func TestStandardAddress(t *testing.T) { func TestParseOneAndImport(t *testing.T) { setupParseTests() - testParseOne := func(input string) (ServerBlock, error) { + testParseOne := func(input string) (serverBlock, error) { p := testParser(input) p.Next() // parseOne doesn't call Next() to start, so we must err := p.parseOne() @@ -69,22 +69,22 @@ func TestParseOneAndImport(t *testing.T) { for i, test := range []struct { input string shouldErr bool - addresses []Address + addresses []address tokens map[string]int // map of directive name to number of tokens expected }{ - {`localhost`, false, []Address{ + {`localhost`, false, []address{ {"localhost", ""}, }, map[string]int{}}, {`localhost - dir1`, false, []Address{ + dir1`, false, []address{ {"localhost", ""}, }, map[string]int{ "dir1": 1, }}, {`localhost:1234 - dir1 foo bar`, false, []Address{ + dir1 foo bar`, false, []address{ {"localhost", "1234"}, }, map[string]int{ "dir1": 3, @@ -92,7 +92,7 @@ func TestParseOneAndImport(t *testing.T) { {`localhost { dir1 - }`, false, []Address{ + }`, false, []address{ {"localhost", ""}, }, map[string]int{ "dir1": 1, @@ -101,7 +101,7 @@ func TestParseOneAndImport(t *testing.T) { {`localhost:1234 { dir1 foo bar dir2 - }`, false, []Address{ + }`, false, []address{ {"localhost", "1234"}, }, map[string]int{ "dir1": 3, @@ -109,7 +109,7 @@ func TestParseOneAndImport(t *testing.T) { }}, {`http://localhost https://localhost - dir1 foo bar`, false, []Address{ + dir1 foo bar`, false, []address{ {"localhost", "http"}, {"localhost", "https"}, }, map[string]int{ @@ -118,7 +118,7 @@ func TestParseOneAndImport(t *testing.T) { {`http://localhost https://localhost { dir1 foo bar - }`, false, []Address{ + }`, false, []address{ {"localhost", "http"}, {"localhost", "https"}, }, map[string]int{ @@ -127,7 +127,7 @@ func TestParseOneAndImport(t *testing.T) { {`http://localhost, https://localhost { dir1 foo bar - }`, false, []Address{ + }`, false, []address{ {"localhost", "http"}, {"localhost", "https"}, }, map[string]int{ @@ -135,13 +135,13 @@ func TestParseOneAndImport(t *testing.T) { }}, {`http://localhost, { - }`, true, []Address{ + }`, true, []address{ {"localhost", "http"}, }, map[string]int{}}, {`host1:80, http://host2.com dir1 foo bar - dir2 baz`, false, []Address{ + dir2 baz`, false, []address{ {"host1", "80"}, {"host2.com", "http"}, }, map[string]int{ @@ -151,7 +151,7 @@ func TestParseOneAndImport(t *testing.T) { {`http://host1.com, http://host2.com, - https://host3.com`, false, []Address{ + https://host3.com`, false, []address{ {"host1.com", "http"}, {"host2.com", "http"}, {"host3.com", "https"}, @@ -161,7 +161,7 @@ func TestParseOneAndImport(t *testing.T) { dir1 foo { bar baz } - dir2`, false, []Address{ + dir2`, false, []address{ {"host1.com", "1234"}, {"host2.com", "https"}, }, map[string]int{ @@ -175,7 +175,7 @@ func TestParseOneAndImport(t *testing.T) { } dir2 { foo bar - }`, false, []Address{ + }`, false, []address{ {"127.0.0.1", ""}, }, map[string]int{ "dir1": 5, @@ -183,13 +183,13 @@ func TestParseOneAndImport(t *testing.T) { }}, {`127.0.0.1 - unknown_directive`, true, []Address{ + unknown_directive`, true, []address{ {"127.0.0.1", ""}, }, map[string]int{}}, {`localhost dir1 { - foo`, true, []Address{ + foo`, true, []address{ {"localhost", ""}, }, map[string]int{ "dir1": 3, @@ -197,7 +197,15 @@ func TestParseOneAndImport(t *testing.T) { {`localhost dir1 { - }`, false, []Address{ + }`, false, []address{ + {"localhost", ""}, + }, map[string]int{ + "dir1": 3, + }}, + + {`localhost + dir1 { + } }`, true, []address{ {"localhost", ""}, }, map[string]int{ "dir1": 3, @@ -209,18 +217,18 @@ func TestParseOneAndImport(t *testing.T) { foo } } - dir2 foo bar`, false, []Address{ + dir2 foo bar`, false, []address{ {"localhost", ""}, }, map[string]int{ "dir1": 7, "dir2": 3, }}, - {``, false, []Address{}, map[string]int{}}, + {``, false, []address{}, map[string]int{}}, {`localhost dir1 arg1 - import import_test1.txt`, false, []Address{ + import import_test1.txt`, false, []address{ {"localhost", ""}, }, map[string]int{ "dir1": 2, @@ -228,16 +236,20 @@ func TestParseOneAndImport(t *testing.T) { "dir3": 1, }}, - {`import import_test2.txt`, false, []Address{ + {`import import_test2.txt`, false, []address{ {"host1", ""}, }, map[string]int{ "dir1": 1, "dir2": 2, }}, - {``, false, []Address{}, map[string]int{}}, + {`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}}, - {`""`, false, []Address{}, map[string]int{}}, + {`import not_found.txt`, true, []address{}, map[string]int{}}, + + {`""`, false, []address{}, map[string]int{}}, + + {``, false, []address{}, map[string]int{}}, } { result, err := testParseOne(test.input) @@ -282,43 +294,43 @@ func TestParseOneAndImport(t *testing.T) { func TestParseAll(t *testing.T) { setupParseTests() - testParseAll := func(input string) ([]ServerBlock, error) { - p := testParser(input) - return p.parseAll() - } - for i, test := range []struct { input string shouldErr bool - numBlocks int + addresses [][]address // addresses per server block, in order }{ - {`localhost`, false, 1}, + {`localhost`, false, [][]address{ + {{"localhost", ""}}, + }}, - {`localhost { - dir1 - }`, false, 1}, + {`localhost:1234`, false, [][]address{ + []address{{"localhost", "1234"}}, + }}, - {`http://localhost https://localhost - dir1 foo bar`, false, 1}, + {`localhost:1234 { + } + localhost:2015 { + }`, false, [][]address{ + []address{{"localhost", "1234"}}, + []address{{"localhost", "2015"}}, + }}, - {`http://localhost, https://localhost { - dir1 foo bar - }`, false, 1}, + {`localhost:1234, http://host2`, false, [][]address{ + []address{{"localhost", "1234"}, {"host2", "http"}}, + }}, - {`http://host1.com, - http://host2.com, - https://host3.com`, false, 1}, + {`localhost:1234, http://host2,`, true, [][]address{}}, - {`host1 { - } - host2 { - }`, false, 2}, - - {`""`, false, 0}, - - {``, false, 0}, + {`http://host1.com, http://host2.com { + } + https://host3.com, https://host4.com { + }`, false, [][]address{ + []address{{"host1.com", "http"}, {"host2.com", "http"}}, + []address{{"host3.com", "https"}, {"host4.com", "https"}}, + }}, } { - results, err := testParseAll(test.input) + p := testParser(test.input) + blocks, err := p.parseAll() if test.shouldErr && err == nil { t.Errorf("Test %d: Expected an error, but didn't get one", i) @@ -327,20 +339,37 @@ func TestParseAll(t *testing.T) { t.Errorf("Test %d: Expected no error, but got: %v", i, err) } - if len(results) != test.numBlocks { + if len(blocks) != len(test.addresses) { t.Errorf("Test %d: Expected %d server blocks, got %d", - i, test.numBlocks, len(results)) + i, len(test.addresses), len(blocks)) continue } + for j, block := range blocks { + if len(block.Addresses) != len(test.addresses[j]) { + t.Errorf("Test %d: Expected %d addresses in block %d, got %d", + i, len(test.addresses[j]), j, len(block.Addresses)) + continue + } + for k, addr := range block.Addresses { + if addr.Host != test.addresses[j][k].Host { + t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'", + i, j, k, test.addresses[j][k].Host, addr.Host) + } + if addr.Port != test.addresses[j][k].Port { + t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'", + i, j, k, test.addresses[j][k].Port, addr.Port) + } + } + } } } func setupParseTests() { // Set up some bogus directives for testing ValidDirectives = map[string]struct{}{ - "dir1": struct{}{}, - "dir2": struct{}{}, - "dir3": struct{}{}, + "dir1": {}, + "dir2": {}, + "dir3": {}, } } diff --git a/config/setup/basicauth_test.go b/config/setup/basicauth_test.go index 7588f0f0..a94d6e69 100644 --- a/config/setup/basicauth_test.go +++ b/config/setup/basicauth_test.go @@ -38,7 +38,7 @@ func TestBasicAuthParse(t *testing.T) { md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` var skipHtpassword bool - htfh, err := ioutil.TempFile("", "basicauth-") + htfh, err := ioutil.TempFile(".", "basicauth-") if err != nil { t.Logf("Error creating temp file (%v), will skip htpassword test", err) skipHtpassword = true diff --git a/config/setup/browse.go b/config/setup/browse.go index fe2f9f9e..cef37c05 100644 --- a/config/setup/browse.go +++ b/config/setup/browse.go @@ -193,6 +193,10 @@ th a { margin-top: 70px; } } + +.name { + white-space: pre; +} @@ -240,7 +244,7 @@ th a { {{if .IsDir}}📂{{else}}📄{{end}} - {{.Name}} + {{.Name}} {{.HumanSize}} {{.HumanModTime "01/02/2006 3:04:05 PM -0700"}} diff --git a/config/setup/controller.go b/config/setup/controller.go index 5631b878..eb9b90cf 100644 --- a/config/setup/controller.go +++ b/config/setup/controller.go @@ -1,11 +1,68 @@ package setup import ( + "fmt" + "net/http" + "strings" + "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/server" ) +// Controller is given to the setup function of middlewares which +// gives them access to be able to read tokens and set config. Each +// virtualhost gets their own server config and dispenser. type Controller struct { *server.Config parse.Dispenser + + // OncePerServerBlock is a function that executes f + // exactly once per server block, no matter how many + // hosts are associated with it. If it is the first + // time, the function f is executed immediately + // (not deferred) and may return an error which is + // returned by OncePerServerBlock. + OncePerServerBlock func(f func() error) error + + // ServerBlockIndex is the 0-based index of the + // server block as it appeared in the input. + ServerBlockIndex int + + // ServerBlockHosts is a list of hosts that are + // associated with this server block. All these + // hosts, consequently, share the same tokens. + ServerBlockHosts []string +} + +// NewTestController creates a new *Controller for +// the input specified, with a filename of "Testfile" +// +// Used primarily for testing but needs to be exported so +// add-ons can use this as a convenience. +func NewTestController(input string) *Controller { + return &Controller{ + Config: &server.Config{ + Root: ".", + }, + Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), + } +} + +// EmptyNext is a no-op function that can be passed into +// middleware.Middleware functions so that the assignment +// to the Next field of the Handler can be tested. +// +// Used primarily for testing but needs to be exported so +// add-ons can use this as a convenience. +var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + return 0, nil +}) + +// SameNext does a pointer comparison between next1 and next2. +// +// Used primarily for testing but needs to be exported so +// add-ons can use this as a convenience. +func SameNext(next1, next2 middleware.Handler) bool { + return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2) } diff --git a/config/setup/controller_test.go b/config/setup/controller_test.go deleted file mode 100644 index 25592940..00000000 --- a/config/setup/controller_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package setup - -import ( - "fmt" - "net/http" - "strings" - - "github.com/mholt/caddy/config/parse" - "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/server" -) - -// NewTestController creates a new *Controller for -// the input specified, with a filename of "Testfile" -func NewTestController(input string) *Controller { - return &Controller{ - Config: &server.Config{}, - Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), - } -} - -// EmptyNext is a no-op function that can be passed into -// middleware.Middleware functions so that the assignment -// to the Next field of the Handler can be tested. -var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { - return 0, nil -}) - -// SameNext does a pointer comparison between next1 and next2. -func SameNext(next1, next2 middleware.Handler) bool { - return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2) -} diff --git a/config/setup/errors.go b/config/setup/errors.go index bc131976..5aa09b9f 100644 --- a/config/setup/errors.go +++ b/config/setup/errors.go @@ -5,7 +5,7 @@ import ( "io" "log" "os" - "path" + "path/filepath" "strconv" "github.com/hashicorp/go-syslog" @@ -105,7 +105,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) { } } else { // Error page; ensure it exists - where = path.Join(c.Root, where) + where = filepath.Join(c.Root, where) f, err := os.Open(where) if err != nil { fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error()) diff --git a/config/setup/fastcgi.go b/config/setup/fastcgi.go index ab21ef1f..a2a7e879 100644 --- a/config/setup/fastcgi.go +++ b/config/setup/fastcgi.go @@ -31,7 +31,7 @@ func FastCGI(c *Controller) (middleware.Middleware, error) { SoftwareName: c.AppName, SoftwareVersion: c.AppVersion, ServerName: c.Host, - ServerPort: c.Port, // BUG: This is not known until the server blocks are split up... + ServerPort: c.Port, } }, nil } diff --git a/config/setup/markdown.go b/config/setup/markdown.go index b67b32bc..65344a74 100644 --- a/config/setup/markdown.go +++ b/config/setup/markdown.go @@ -68,62 +68,8 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) { // Load any other configuration parameters for c.NextBlock() { - switch c.Val() { - case "ext": - exts := c.RemainingArgs() - if len(exts) == 0 { - return mdconfigs, c.ArgErr() - } - md.Extensions = append(md.Extensions, exts...) - case "css": - if !c.NextArg() { - return mdconfigs, c.ArgErr() - } - md.Styles = append(md.Styles, c.Val()) - case "js": - if !c.NextArg() { - return mdconfigs, c.ArgErr() - } - md.Scripts = append(md.Scripts, c.Val()) - case "template": - tArgs := c.RemainingArgs() - switch len(tArgs) { - case 0: - return mdconfigs, c.ArgErr() - case 1: - if _, ok := md.Templates[markdown.DefaultTemplate]; ok { - return mdconfigs, c.Err("only one default template is allowed, use alias.") - } - fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]) - md.Templates[markdown.DefaultTemplate] = fpath - case 2: - fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]) - md.Templates[tArgs[0]] = fpath - default: - return mdconfigs, c.ArgErr() - } - case "sitegen": - if c.NextArg() { - md.StaticDir = path.Join(c.Root, c.Val()) - } else { - md.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir) - } - if c.NextArg() { - // only 1 argument allowed - return mdconfigs, c.ArgErr() - } - case "dev": - if c.NextArg() { - md.Development = strings.ToLower(c.Val()) == "true" - } else { - md.Development = true - } - if c.NextArg() { - // only 1 argument allowed - return mdconfigs, c.ArgErr() - } - default: - return mdconfigs, c.Err("Expected valid markdown configuration property") + if err := loadParams(c, md); err != nil { + return mdconfigs, err } } @@ -137,3 +83,70 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) { return mdconfigs, nil } + +func loadParams(c *Controller, mdc *markdown.Config) error { + switch c.Val() { + case "ext": + exts := c.RemainingArgs() + if len(exts) == 0 { + return c.ArgErr() + } + mdc.Extensions = append(mdc.Extensions, exts...) + return nil + case "css": + if !c.NextArg() { + return c.ArgErr() + } + mdc.Styles = append(mdc.Styles, c.Val()) + return nil + case "js": + if !c.NextArg() { + return c.ArgErr() + } + mdc.Scripts = append(mdc.Scripts, c.Val()) + return nil + case "template": + tArgs := c.RemainingArgs() + switch len(tArgs) { + case 0: + return c.ArgErr() + case 1: + if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok { + return c.Err("only one default template is allowed, use alias.") + } + fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0])) + mdc.Templates[markdown.DefaultTemplate] = fpath + return nil + case 2: + fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1])) + mdc.Templates[tArgs[0]] = fpath + return nil + default: + return c.ArgErr() + } + case "sitegen": + if c.NextArg() { + mdc.StaticDir = path.Join(c.Root, c.Val()) + } else { + mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir) + } + if c.NextArg() { + // only 1 argument allowed + return c.ArgErr() + } + return nil + case "dev": + if c.NextArg() { + mdc.Development = strings.ToLower(c.Val()) == "true" + } else { + mdc.Development = true + } + if c.NextArg() { + // only 1 argument allowed + return c.ArgErr() + } + return nil + default: + return c.Err("Expected valid markdown configuration property") + } +} diff --git a/config/setup/markdown_test.go b/config/setup/markdown_test.go index 1ada8a1f..5bf012b0 100644 --- a/config/setup/markdown_test.go +++ b/config/setup/markdown_test.go @@ -1,6 +1,7 @@ package setup import ( + "bytes" "fmt" "io/ioutil" "net/http" @@ -92,7 +93,7 @@ func TestMarkdownStaticGen(t *testing.T) { t.Fatalf("An error occured when getting the file content: %v", err) } - expectedBody := ` + expectedBody := []byte(` first_post @@ -104,9 +105,10 @@ func TestMarkdownStaticGen(t *testing.T) { -` - if string(html) != expectedBody { - t.Fatalf("Expected file content: %v got: %v", expectedBody, html) +`) + + if !bytes.Equal(html, expectedBody) { + t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html)) } fp := filepath.Join(c.Root, markdown.DefaultStaticDir) diff --git a/config/setup/mime.go b/config/setup/mime.go new file mode 100644 index 00000000..760056eb --- /dev/null +++ b/config/setup/mime.go @@ -0,0 +1,62 @@ +package setup + +import ( + "fmt" + "strings" + + "github.com/mholt/caddy/middleware" + "github.com/mholt/caddy/middleware/mime" +) + +// Mime configures a new mime middleware instance. +func Mime(c *Controller) (middleware.Middleware, error) { + configs, err := mimeParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return mime.Mime{Next: next, Configs: configs} + }, nil +} + +func mimeParse(c *Controller) ([]mime.Config, error) { + var configs []mime.Config + + for c.Next() { + // At least one extension is required + + args := c.RemainingArgs() + switch len(args) { + case 2: + if err := validateExt(args[0]); err != nil { + return configs, err + } + configs = append(configs, mime.Config{Ext: args[0], ContentType: args[1]}) + case 1: + return configs, c.ArgErr() + case 0: + for c.NextBlock() { + ext := c.Val() + if err := validateExt(ext); err != nil { + return configs, err + } + if !c.NextArg() { + return configs, c.ArgErr() + } + configs = append(configs, mime.Config{Ext: ext, ContentType: c.Val()}) + } + } + + } + + return configs, nil +} + +// validateExt checks for valid file name extension. +func validateExt(ext string) error { + if !strings.HasPrefix(ext, ".") { + return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext) + } + return nil +} diff --git a/config/setup/mime_test.go b/config/setup/mime_test.go new file mode 100644 index 00000000..7f8d8de6 --- /dev/null +++ b/config/setup/mime_test.go @@ -0,0 +1,59 @@ +package setup + +import ( + "testing" + + "github.com/mholt/caddy/middleware/mime" +) + +func TestMime(t *testing.T) { + + c := NewTestController(`mime .txt text/plain`) + + mid, err := Mime(c) + if err != nil { + t.Errorf("Expected no errors, but got: %v", err) + } + if mid == nil { + t.Fatal("Expected middleware, was nil instead") + } + + handler := mid(EmptyNext) + myHandler, ok := handler.(mime.Mime) + if !ok { + t.Fatalf("Expected handler to be type Mime, got: %#v", handler) + } + + if !SameNext(myHandler.Next, EmptyNext) { + t.Error("'Next' field of handler was not set properly") + } + + tests := []struct { + input string + shouldErr bool + }{ + {`mime {`, true}, + {`mime {}`, true}, + {`mime a b`, true}, + {`mime a {`, true}, + {`mime { txt f } `, true}, + {`mime { html } `, true}, + {`mime { + .html text/html + .txt text/plain + } `, false}, + {`mime { .html text/html } `, false}, + {`mime { .html + } `, true}, + {`mime .txt text/plain`, false}, + } + for i, test := range tests { + c := NewTestController(test.input) + m, err := mimeParse(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil %v", i, m) + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + } + } +} diff --git a/config/setup/proxy.go b/config/setup/proxy.go index 42aebf9d..3011cb0e 100644 --- a/config/setup/proxy.go +++ b/config/setup/proxy.go @@ -7,11 +7,11 @@ import ( // Proxy configures a new Proxy middleware instance. func Proxy(c *Controller) (middleware.Middleware, error) { - if upstreams, err := proxy.NewStaticUpstreams(c.Dispenser); err == nil { - return func(next middleware.Handler) middleware.Handler { - return proxy.Proxy{Next: next, Upstreams: upstreams} - }, nil - } else { + upstreams, err := proxy.NewStaticUpstreams(c.Dispenser) + if err != nil { return nil, err } + return func(next middleware.Handler) middleware.Handler { + return proxy.Proxy{Next: next, Upstreams: upstreams} + }, nil } diff --git a/config/setup/redir.go b/config/setup/redir.go index 008ba6c7..63488f4a 100644 --- a/config/setup/redir.go +++ b/config/setup/redir.go @@ -37,13 +37,13 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { // checkAndSaveRule checks the rule for validity (except the redir code) // and saves it if it's valid, or returns an error. checkAndSaveRule := func(rule redirect.Rule) error { - if rule.From == rule.To { + if rule.FromPath == rule.To { return c.Err("'from' and 'to' values of redirect rule cannot be the same") } for _, otherRule := range redirects { - if otherRule.From == rule.From { - return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.From, otherRule.To) + if otherRule.FromPath == rule.FromPath { + return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To) } } @@ -60,6 +60,12 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { var rule redirect.Rule + if c.Config.TLS.Enabled { + rule.FromScheme = "https" + } else { + rule.FromScheme = "http" + } + // Set initial redirect code // BUG: If the code is specified for a whole block and that code is invalid, // the line number will appear on the first line inside the block, even if that @@ -84,15 +90,15 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { // To specified (catch-all redirect) // Not sure why user is doing this in a table, as it causes all other redirects to be ignored. // As such, this feature remains undocumented. - rule.From = "/" + rule.FromPath = "/" rule.To = insideArgs[0] case 2: // From and To specified - rule.From = insideArgs[0] + rule.FromPath = insideArgs[0] rule.To = insideArgs[1] case 3: // From, To, and Code specified - rule.From = insideArgs[0] + rule.FromPath = insideArgs[0] rule.To = insideArgs[1] err := setRedirCode(insideArgs[2], &rule) if err != nil { @@ -110,16 +116,23 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { if !hadOptionalBlock { var rule redirect.Rule + + if c.Config.TLS.Enabled { + rule.FromScheme = "https" + } else { + rule.FromScheme = "http" + } + rule.Code = http.StatusMovedPermanently // default switch len(args) { case 1: // To specified (catch-all redirect) - rule.From = "/" + rule.FromPath = "/" rule.To = args[0] case 2: // To and Code specified (catch-all redirect) - rule.From = "/" + rule.FromPath = "/" rule.To = args[0] err := setRedirCode(args[1], &rule) if err != nil { @@ -127,7 +140,7 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { } case 3: // From, To, and Code specified - rule.From = args[0] + rule.FromPath = args[0] rule.To = args[1] err := setRedirCode(args[2], &rule) if err != nil { @@ -149,12 +162,12 @@ func redirParse(c *Controller) ([]redirect.Rule, error) { // httpRedirs is a list of supported HTTP redirect codes. var httpRedirs = map[string]int{ - "300": 300, // Multiple Choices - "301": 301, // Moved Permanently - "302": 302, // Found (NOT CORRECT for "Temporary Redirect", see 307) - "303": 303, // See Other - "304": 304, // Not Modified - "305": 305, // Use Proxy - "307": 307, // Temporary Redirect + "300": http.StatusMultipleChoices, + "301": http.StatusMovedPermanently, + "302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307) + "303": http.StatusSeeOther, + "304": http.StatusNotModified, + "305": http.StatusUseProxy, + "307": http.StatusTemporaryRedirect, "308": 308, // Permanent Redirect } diff --git a/config/setup/root_test.go b/config/setup/root_test.go new file mode 100644 index 00000000..8b38e6d0 --- /dev/null +++ b/config/setup/root_test.go @@ -0,0 +1,108 @@ +package setup + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRoot(t *testing.T) { + + // Predefined error substrings + parseErrContent := "Parse error:" + unableToAccessErrContent := "Unable to access root path" + + existingDirPath, err := getTempDirPath() + if err != nil { + t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) + } + + nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir") + + existingFile, err := ioutil.TempFile("", "root_test") + if err != nil { + t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err) + } + defer func() { + existingFile.Close() + os.Remove(existingFile.Name()) + }() + + inaccessiblePath := getInaccessiblePath(existingFile.Name()) + + tests := []struct { + input string + shouldErr bool + expectedRoot string // expected root, set to the controller. Empty for negative cases. + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + { + fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "", + }, + { + fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "", + }, + // negative + { + `root `, true, "", parseErrContent, + }, + { + fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent, + }, + { + fmt.Sprintf(`root { + %s + }`, existingDirPath), true, "", parseErrContent, + }, + } + + for i, test := range tests { + c := NewTestController(test.input) + mid, err := Root(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + } + + // the Root method always returns a nil middleware + if mid != nil { + t.Errorf("Middware, returned from Root() was not nil: %v", mid) + } + + // check c.Root only if we are in a positive test. + if !test.shouldErr && test.expectedRoot != c.Root { + t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root) + } + } +} + +// getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned. +func getTempDirPath() (string, error) { + tempDir := os.TempDir() + + _, err := os.Stat(tempDir) + if err != nil { + return "", err + } + + return tempDir, nil +} + +func getInaccessiblePath(file string) string { + // null byte in filename is not allowed on Windows AND unix + return filepath.Join("C:", "file\x00name") +} diff --git a/config/setup/startupshutdown.go b/config/setup/startupshutdown.go index ab7ce03e..e4d87305 100644 --- a/config/setup/startupshutdown.go +++ b/config/setup/startupshutdown.go @@ -20,6 +20,8 @@ func Shutdown(c *Controller) (middleware.Middleware, error) { // using c to parse the line. It appends the callback function // to the list of callback functions passed in by reference. func registerCallback(c *Controller, list *[]func() error) error { + var funcs []func() error + for c.Next() { args := c.RemainingArgs() if len(args) == 0 { @@ -46,13 +48,15 @@ func registerCallback(c *Controller, list *[]func() error) error { if nonblock { return cmd.Start() - } else { - return cmd.Run() } + return cmd.Run() } - *list = append(*list, fn) + funcs = append(funcs, fn) } - return nil + return c.OncePerServerBlock(func() error { + *list = append(*list, funcs...) + return nil + }) } diff --git a/config/setup/websocket.go b/config/setup/websocket.go index 9178bd13..33df76d7 100644 --- a/config/setup/websocket.go +++ b/config/setup/websocket.go @@ -2,26 +2,26 @@ package setup import ( "github.com/mholt/caddy/middleware" - "github.com/mholt/caddy/middleware/websockets" + "github.com/mholt/caddy/middleware/websocket" ) -// WebSocket configures a new WebSockets middleware instance. +// WebSocket configures a new WebSocket middleware instance. func WebSocket(c *Controller) (middleware.Middleware, error) { websocks, err := webSocketParse(c) if err != nil { return nil, err } - websockets.GatewayInterface = c.AppName + "-CGI/1.1" - websockets.ServerSoftware = c.AppName + "/" + c.AppVersion + websocket.GatewayInterface = c.AppName + "-CGI/1.1" + websocket.ServerSoftware = c.AppName + "/" + c.AppVersion return func(next middleware.Handler) middleware.Handler { - return websockets.WebSockets{Next: next, Sockets: websocks} + return websocket.WebSocket{Next: next, Sockets: websocks} }, nil } -func webSocketParse(c *Controller) ([]websockets.Config, error) { - var websocks []websockets.Config +func webSocketParse(c *Controller) ([]websocket.Config, error) { + var websocks []websocket.Config var respawn bool optionalBlock := func() (hadBlock bool, err error) { @@ -74,7 +74,7 @@ func webSocketParse(c *Controller) ([]websockets.Config, error) { return nil, err } - websocks = append(websocks, websockets.Config{ + websocks = append(websocks, websocket.Config{ Path: path, Command: cmd, Arguments: args, diff --git a/config/setup/websocket_test.go b/config/setup/websocket_test.go index 86af253d..750f2a1d 100644 --- a/config/setup/websocket_test.go +++ b/config/setup/websocket_test.go @@ -1,8 +1,9 @@ package setup import ( - "github.com/mholt/caddy/middleware/websockets" "testing" + + "github.com/mholt/caddy/middleware/websocket" ) func TestWebSocket(t *testing.T) { @@ -20,10 +21,10 @@ func TestWebSocket(t *testing.T) { } handler := mid(EmptyNext) - myHandler, ok := handler.(websockets.WebSockets) + myHandler, ok := handler.(websocket.WebSocket) if !ok { - t.Fatalf("Expected handler to be type WebSockets, got: %#v", handler) + t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler) } if myHandler.Sockets[0].Path != "/" { @@ -38,15 +39,15 @@ func TestWebSocketParse(t *testing.T) { tests := []struct { inputWebSocketConfig string shouldErr bool - expectedWebSocketConfig []websockets.Config + expectedWebSocketConfig []websocket.Config }{ - {`websocket /api1 cat`, false, []websockets.Config{{ + {`websocket /api1 cat`, false, []websocket.Config{{ Path: "/api1", Command: "cat", }}}, {`websocket /api3 cat - websocket /api4 cat `, false, []websockets.Config{{ + websocket /api4 cat `, false, []websocket.Config{{ Path: "/api3", Command: "cat", }, { diff --git a/dist/CHANGES.txt b/dist/CHANGES.txt index 9a1c0036..6a2c201c 100644 --- a/dist/CHANGES.txt +++ b/dist/CHANGES.txt @@ -1,12 +1,18 @@ CHANGES +- New directive 'mime' to customize Content-Type based on file extension + + +0.7.6 (September 28, 2015) +- Pass in simple Caddyfile as command line arguments - basicauth: Support for legacy htpasswd files -- browse: JSON response with file listing given Accept header +- browse: JSON response with file listing - core: Caddyfile as command line argument - errors: Can write full stack trace to HTTP response for debugging - errors, log: Roll log files after certain size or age - proxy: Fix for 32-bit architectures +- rewrite: Better compatibility with fastcgi and PHP apps - templates: Added .StripExt and .StripHTML methods - Internal improvements and minor bug fixes diff --git a/dist/README.txt b/dist/README.txt index 2a3cbfc0..de3fde6a 100644 --- a/dist/README.txt +++ b/dist/README.txt @@ -1,4 +1,4 @@ -CADDY 0.7.5 +CADDY 0.7.6 Website https://caddyserver.com diff --git a/main.go b/main.go index 7187567d..5405abe9 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ var ( func init() { flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")") - flag.BoolVar(&app.Http2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib + flag.BoolVar(&app.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib flag.BoolVar(&app.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") @@ -51,7 +51,7 @@ func main() { log.Fatal(err) } - // Load address configurations from highest priority input + // Load config from file addresses, err := loadConfigs() if err != nil { log.Fatal(err) @@ -63,7 +63,7 @@ func main() { if err != nil { log.Fatal(err) } - s.HTTP2 = app.Http2 // TODO: This setting is temporary + s.HTTP2 = app.HTTP2 // TODO: This setting is temporary app.Wg.Add(1) go func(s *server.Server) { defer app.Wg.Done() @@ -125,10 +125,9 @@ func isLocalhost(s string) bool { // loadConfigs loads configuration from a file or stdin (piped). // The configurations are grouped by bind address. -// Configuration is obtained from one of three sources, tried +// Configuration is obtained from one of four sources, tried // in this order: 1. -conf flag, 2. stdin, 3. command line argument 4. Caddyfile. -// If none of those are available, a default configuration is -// loaded. +// If none of those are available, a default configuration is loaded. func loadConfigs() (config.Group, error) { // -conf flag if conf != "" { diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index eeeb5476..14e7d210 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -78,6 +78,7 @@ type Rule struct { Resources []string } +// PasswordMatcher determines whether a password mathes a rule. type PasswordMatcher func(pw string) bool var ( @@ -137,6 +138,8 @@ func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error { return scanner.Err() } +// PlainMatcher returns a PasswordMatcher that does a constant-time +// byte-wise comparison. func PlainMatcher(passw string) PasswordMatcher { return func(pw string) bool { return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1 diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index aa1fc244..aad5ed39 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "testing" "github.com/mholt/caddy/middleware" @@ -124,15 +125,18 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61` t.Skipf("Error creating temp file (%v), will skip htpassword test") return } + defer os.Remove(htfh.Name()) if _, err = htfh.Write([]byte(htpasswdFile)); err != nil { t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err) } htfh.Close() - defer os.Remove(htfh.Name()) for i, username := range []string{"sha1", "md5"} { rule := Rule{Username: username, Resources: []string{"/testing"}} - if rule.Password, err = GetHtpasswdMatcher(htfh.Name(), rule.Username, "/"); err != nil { + + siteRoot := filepath.Dir(htfh.Name()) + filename := filepath.Base(htfh.Name()) + if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil { t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err) } t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password) diff --git a/middleware/browse/browse.go b/middleware/browse/browse.go index a86c5f02..5a8c229b 100644 --- a/middleware/browse/browse.go +++ b/middleware/browse/browse.go @@ -242,24 +242,31 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order") // If the query 'sort' or 'order' is empty, check the cookies - if listing.Sort == "" || listing.Order == "" { + if listing.Sort == "" { sortCookie, sortErr := r.Cookie("sort") - orderCookie, orderErr := r.Cookie("order") - // if there's no sorting values in the cookies, default to "name" and "asc" - if sortErr != nil || orderErr != nil { + if sortErr != nil { listing.Sort = "name" - listing.Order = "asc" } else { // if we have values in the cookies, use them listing.Sort = sortCookie.Value - listing.Order = orderCookie.Value } - } else { // save the query value of 'sort' and 'order' as cookies http.SetCookie(w, &http.Cookie{Name: "sort", Value: listing.Sort, Path: "/"}) http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"}) } + if listing.Order == "" { + orderCookie, orderErr := r.Cookie("order") + // if there's no sorting values in the cookies, default to "name" and "asc" + if orderErr != nil { + listing.Order = "asc" + } else { // if we have values in the cookies, use them + listing.Order = orderCookie.Value + } + } else { // save the query value of 'sort' and 'order' as cookies + http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"}) + } + // Apply the sorting listing.applySort() diff --git a/middleware/browse/browse_test.go b/middleware/browse/browse_test.go index 05ccff14..2d653c98 100644 --- a/middleware/browse/browse_test.go +++ b/middleware/browse/browse_test.go @@ -2,15 +2,17 @@ package browse import ( "encoding/json" - "github.com/mholt/caddy/middleware" "net/http" "net/http/httptest" "net/url" "os" + "path/filepath" "sort" "testing" "text/template" "time" + + "github.com/mholt/caddy/middleware" ) // "sort" package has "IsSorted" function, but no "IsReversed"; @@ -115,7 +117,7 @@ func TestBrowseTemplate(t *testing.T) { }), Root: "./testdata", Configs: []Config{ - Config{ + { PathScope: "/photos", Template: tmpl, }, @@ -129,9 +131,9 @@ func TestBrowseTemplate(t *testing.T) { rec := httptest.NewRecorder() - b.ServeHTTP(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, rec.Code) + code, err := b.ServeHTTP(rec, req) + if code != http.StatusOK { + t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code) } respBody := rec.Body.String() @@ -149,6 +151,8 @@ func TestBrowseTemplate(t *testing.T) { test2.html
+test3.html
+ ` @@ -168,37 +172,19 @@ func TestBrowseJson(t *testing.T) { }), Root: "./testdata", Configs: []Config{ - Config{ - PathScope: "/photos", + { + PathScope: "/photos/", }, }, } - req, err := http.NewRequest("GET", "/photos/", nil) - if err != nil { - t.Fatalf("Test: Could not create HTTP request: %v", err) - } - req.Header.Set("Accept", "application/json") - rec := httptest.NewRecorder() - - b.ServeHTTP(rec, req) - if rec.Code != http.StatusOK { - t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, rec.Code) - } - if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { - t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type")) - } - - actualJsonResponseString := rec.Body.String() - - //generating the listing to compare it with the response body - file, err := os.Open(b.Root + req.URL.Path) + //Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results + testDataPath := b.Root + "/photos/" + file, err := os.Open(testDataPath) if err != nil { if os.IsPermission(err) { t.Fatalf("Os Permission Error") - } - } defer file.Close() @@ -207,9 +193,18 @@ func TestBrowseJson(t *testing.T) { t.Fatalf("Unable to Read Contents of the directory") } var fileinfos []FileInfo - for _, f := range files { + + for i, f := range files { name := f.Name() + // Tests fail in CI environment because all file mod times are the same for + // some reason, making the sorting unpredictable. To hack around this, + // we ensure here that each file has a different mod time. + chTime := f.ModTime().Add(-(time.Duration(i) * time.Second)) + if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil { + t.Fatal(err) + } + if f.IsDir() { name += "/" } @@ -221,23 +216,103 @@ func TestBrowseJson(t *testing.T) { Name: f.Name(), Size: f.Size(), URL: url.String(), - ModTime: f.ModTime(), + ModTime: chTime, Mode: f.Mode(), }) } - listing := Listing{ - Items: fileinfos, - } - listing.Sort = "name" - listing.Order = "asc" - listing.applySort() + listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests - marsh, err := json.Marshal(listing.Items) - if err != nil { - t.Fatalf("Unable to Marshal the listing ") + tests := []struct { + QueryUrl string + SortBy string + OrderBy string + Limit int + shouldErr bool + expectedResult []FileInfo + }{ + //test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending + //without the limit query entire listing will be produced + {"/", "", "", -1, false, listing.Items}, + //test case 2: limit is set to 1, orderBy and sortBy is default + {"/?limit=1", "", "", 1, false, listing.Items[:1]}, + //test case 3 : if the listing request is bigger than total size of listing then it should return everything + {"/?limit=100000000", "", "", 100000000, false, listing.Items}, + //test case 4 : testing for negative limit + {"/?limit=-1", "", "", -1, false, listing.Items}, + //test case 5 : testing with limit set to -1 and order set to descending + {"/?limit=-1&order=desc", "", "desc", -1, false, listing.Items}, + //test case 6 : testing with limit set to 2 and order set to descending + {"/?limit=2&order=desc", "", "desc", 2, false, listing.Items}, + //test case 7 : testing with limit set to 3 and order set to descending + {"/?limit=3&order=desc", "", "desc", 3, false, listing.Items}, + //test case 8 : testing with limit set to 3 and order set to ascending + {"/?limit=3&order=asc", "", "asc", 3, false, listing.Items}, + //test case 9 : testing with limit set to 1111111 and order set to ascending + {"/?limit=1111111&order=asc", "", "asc", 1111111, false, listing.Items}, + //test case 10 : testing with limit set to default and order set to ascending and sorting by size + {"/?order=asc&sort=size", "size", "asc", -1, false, listing.Items}, + //test case 11 : testing with limit set to default and order set to ascending and sorting by last modified + {"/?order=asc&sort=time", "time", "asc", -1, false, listing.Items}, + //test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified + {"/?order=asc&sort=time&limit=1", "time", "asc", 1, false, listing.Items}, + //test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified + {"/?order=asc&sort=time&limit=-100", "time", "asc", -100, false, listing.Items}, + //test case 14 : testing with limit set to -100 and order set to ascending and sorting by size + {"/?order=asc&sort=size&limit=-100", "size", "asc", -100, false, listing.Items}, } - expectedJsonString := string(marsh) - if actualJsonResponseString != expectedJsonString { - t.Errorf("Json response string doesnt match the expected Json response ") + + for i, test := range tests { + var marsh []byte + req, err := http.NewRequest("GET", "/photos"+test.QueryUrl, nil) + + if err == nil && test.shouldErr { + t.Errorf("Test %d didn't error, but it should have", i) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) + } + + req.Header.Set("Accept", "application/json") + rec := httptest.NewRecorder() + + code, err := b.ServeHTTP(rec, req) + + if code != http.StatusOK { + t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code) + } + if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" { + t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type")) + } + + actualJSONResponse := rec.Body.String() + copyOflisting := listing + if test.SortBy == "" { + copyOflisting.Sort = "name" + } else { + copyOflisting.Sort = test.SortBy + } + if test.OrderBy == "" { + copyOflisting.Order = "asc" + } else { + copyOflisting.Order = test.OrderBy + } + + copyOflisting.applySort() + + limit := test.Limit + if limit <= len(copyOflisting.Items) && limit > 0 { + marsh, err = json.Marshal(copyOflisting.Items[:limit]) + } else { // if the 'limit' query is empty, or has the wrong value, list everything + marsh, err = json.Marshal(copyOflisting.Items) + } + + if err != nil { + t.Fatalf("Unable to Marshal the listing ") + } + expectedJSON := string(marsh) + + if actualJSONResponse != expectedJSON { + t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n", + i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse) + } } } diff --git a/middleware/browse/testdata/photos/test3.html b/middleware/browse/testdata/photos/test3.html new file mode 100644 index 00000000..6c70af2f --- /dev/null +++ b/middleware/browse/testdata/photos/test3.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/middleware/commands_test.go b/middleware/commands_test.go new file mode 100644 index 00000000..3a5b3334 --- /dev/null +++ b/middleware/commands_test.go @@ -0,0 +1,138 @@ +package middleware + +import ( + "fmt" + "strings" + "testing" +) + +func TestSplitCommandAndArgs(t *testing.T) { + var parseErrorContent = "error parsing command:" + var noCommandErrContent = "no command contained in" + + tests := []struct { + input string + expectedCommand string + expectedArgs []string + expectedErrContent string + }{ + // Test case 0 - emtpy command + { + input: ``, + expectedCommand: ``, + expectedArgs: nil, + expectedErrContent: noCommandErrContent, + }, + // Test case 1 - command without arguments + { + input: `command`, + expectedCommand: `command`, + expectedArgs: nil, + expectedErrContent: ``, + }, + // Test case 2 - command with single argument + { + input: `command arg1`, + expectedCommand: `command`, + expectedArgs: []string{`arg1`}, + expectedErrContent: ``, + }, + // Test case 3 - command with multiple arguments + { + input: `command arg1 arg2`, + expectedCommand: `command`, + expectedArgs: []string{`arg1`, `arg2`}, + expectedErrContent: ``, + }, + // Test case 4 - command with single argument with space character - in quotes + { + input: `command "arg1 arg1"`, + expectedCommand: `command`, + expectedArgs: []string{`arg1 arg1`}, + expectedErrContent: ``, + }, + // Test case 4 - command with single argument with space character - escaped + { + input: `command arg1\ arg1`, + expectedCommand: `command`, + expectedArgs: []string{`arg1 arg1`}, + expectedErrContent: ``, + }, + // Test case 6 - command with escaped quote character + { + input: `command "arg1 \" arg1"`, + expectedCommand: `command`, + expectedArgs: []string{`arg1 " arg1`}, + expectedErrContent: ``, + }, + // Test case 7 - command with escaped backslash + { + input: `command '\arg1'`, + expectedCommand: `command`, + expectedArgs: []string{`\arg1`}, + expectedErrContent: ``, + }, + // Test case 8 - command with comments + { + input: `command arg1 #comment1 comment2`, + expectedCommand: `command`, + expectedArgs: []string{`arg1`}, + expectedErrContent: "", + }, + // Test case 9 - command with multiple spaces and tab character + { + input: "command arg1 arg2\targ3", + expectedCommand: `command`, + expectedArgs: []string{`arg1`, `arg2`, "arg3"}, + expectedErrContent: "", + }, + // Test case 10 - command with unclosed quotes + { + input: `command "arg1 arg2`, + expectedCommand: "", + expectedArgs: nil, + expectedErrContent: parseErrorContent, + }, + // Test case 11 - command with unclosed quotes + { + input: `command 'arg1 arg2"`, + expectedCommand: "", + expectedArgs: nil, + expectedErrContent: parseErrorContent, + }, + } + + for i, test := range tests { + errorPrefix := fmt.Sprintf("Test [%d]: ", i) + errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input) + actualCommand, actualArgs, actualErr := SplitCommandAndArgs(test.input) + + // test if error matches expectation + if test.expectedErrContent != "" { + if actualErr == nil { + t.Errorf(errorPrefix+"Expected error with content [%s], found no error."+errorSuffix, test.expectedErrContent) + } else if !strings.Contains(actualErr.Error(), test.expectedErrContent) { + t.Errorf(errorPrefix+"Expected error with content [%s], found [%v]."+errorSuffix, test.expectedErrContent, actualErr) + } + } else if actualErr != nil { + t.Errorf(errorPrefix+"Expected no error, found [%v]."+errorSuffix, actualErr) + } + + // test if command matches + if test.expectedCommand != actualCommand { + t.Errorf("Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand) + } + + // test if arguments match + if len(test.expectedArgs) != len(actualArgs) { + t.Errorf("Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs) + } + + for j, actualArg := range actualArgs { + expectedArg := test.expectedArgs[j] + if actualArg != expectedArg { + t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg) + } + } + } +} diff --git a/middleware/errors/errors.go b/middleware/errors/errors.go index e148899a..e9eef90e 100644 --- a/middleware/errors/errors.go +++ b/middleware/errors/errors.go @@ -37,9 +37,8 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er w.WriteHeader(status) fmt.Fprintln(w, errMsg) return 0, err // returning < 400 signals that a response has been written - } else { - h.Log.Println(errMsg) } + h.Log.Println(errMsg) } if status >= 400 { diff --git a/middleware/fastcgi/fastcgi.go b/middleware/fastcgi/fastcgi.go old mode 100644 new mode 100755 index 85e5b522..eef6f4bb --- a/middleware/fastcgi/fastcgi.go +++ b/middleware/fastcgi/fastcgi.go @@ -58,17 +58,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) } // Connect to FastCGI gateway - var fcgi *FCGIClient - - // check if unix socket or tcp - if strings.HasPrefix(rule.Address, "/") || strings.HasPrefix(rule.Address, "unix:") { - if strings.HasPrefix(rule.Address, "unix:") { - rule.Address = rule.Address[len("unix:"):] - } - fcgi, err = Dial("unix", rule.Address) - } else { - fcgi, err = Dial("tcp", rule.Address) - } + fcgi, err := getClient(&rule) if err != nil { return http.StatusBadGateway, err } @@ -102,13 +92,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) return http.StatusBadGateway, err } - // Write the response header - for key, vals := range resp.Header { - for _, val := range vals { - w.Header().Add(key, val) - } - } - w.WriteHeader(resp.StatusCode) + writeHeader(w, resp) // Write the response body // TODO: If this has an error, the response will already be @@ -126,6 +110,26 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) return h.Next.ServeHTTP(w, r) } +func getClient(r *Rule) (*FCGIClient, error) { + // check if unix socket or TCP + if trim := strings.HasPrefix(r.Address, "unix"); strings.HasPrefix(r.Address, "/") || trim { + if trim { + r.Address = r.Address[len("unix:"):] + } + return Dial("unix", r.Address) + } + return Dial("tcp", r.Address) +} + +func writeHeader(w http.ResponseWriter, r *http.Response) { + for key, vals := range r.Header { + for _, val := range vals { + w.Header().Add(key, val) + } + } + w.WriteHeader(r.StatusCode) +} + func (h Handler) exists(path string) bool { if _, err := os.Stat(h.Root + path); err == nil { return true @@ -166,6 +170,20 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string] scriptFilename = absPath } + // Strip PATH_INFO from SCRIPT_NAME + scriptName = strings.TrimSuffix(scriptName, pathInfo) + + // Get the request URI. The request URI might be as it came in over the wire, + // or it might have been rewritten internally by the rewrite middleware (see issue #256). + // If it was rewritten, there will be a header indicating the original URL, + // which is needed to get the correct RequestURI value for PHP apps. + const internalRewriteFieldName = "Caddy-Rewrite-Original-URI" + reqURI := r.URL.RequestURI() + if origURI := r.Header.Get(internalRewriteFieldName); origURI != "" { + reqURI = origURI + r.Header.Del(internalRewriteFieldName) + } + // Some variables are unused but cleared explicitly to prevent // the parent environment from interfering. env = map[string]string{ @@ -192,7 +210,7 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string] "DOCUMENT_ROOT": h.AbsRoot, "DOCUMENT_URI": docURI, "HTTP_HOST": r.Host, // added here, since not always part of headers - "REQUEST_URI": r.URL.RequestURI(), + "REQUEST_URI": reqURI, "SCRIPT_FILENAME": scriptFilename, "SCRIPT_NAME": scriptName, } diff --git a/middleware/fastcgi/fcgiclient.go b/middleware/fastcgi/fcgiclient.go index b657a3b3..de46a4e8 100644 --- a/middleware/fastcgi/fcgiclient.go +++ b/middleware/fastcgi/fcgiclient.go @@ -30,45 +30,45 @@ import ( "sync" ) -const FCGI_LISTENSOCK_FILENO uint8 = 0 -const FCGI_HEADER_LEN uint8 = 8 -const VERSION_1 uint8 = 1 -const FCGI_NULL_REQUEST_ID uint8 = 0 -const FCGI_KEEP_CONN uint8 = 1 +const FCGIListenSockFileno uint8 = 0 +const FCGIHeaderLen uint8 = 8 +const Version1 uint8 = 1 +const FCGINullRequestID uint8 = 0 +const FCGIKeepConn uint8 = 1 const doubleCRLF = "\r\n\r\n" const ( - FCGI_BEGIN_REQUEST uint8 = iota + 1 - FCGI_ABORT_REQUEST - FCGI_END_REQUEST - FCGI_PARAMS - FCGI_STDIN - FCGI_STDOUT - FCGI_STDERR - FCGI_DATA - FCGI_GET_VALUES - FCGI_GET_VALUES_RESULT - FCGI_UNKNOWN_TYPE - FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE + BeginRequest uint8 = iota + 1 + AbortRequest + EndRequest + Params + Stdin + Stdout + Stderr + Data + GetValues + GetValuesResult + UnknownType + MaxType = UnknownType ) const ( - FCGI_RESPONDER uint8 = iota + 1 - FCGI_AUTHORIZER - FCGI_FILTER + Responder uint8 = iota + 1 + Authorizer + Filter ) const ( - FCGI_REQUEST_COMPLETE uint8 = iota - FCGI_CANT_MPX_CONN - FCGI_OVERLOADED - FCGI_UNKNOWN_ROLE + RequestComplete uint8 = iota + CantMultiplexConns + Overloaded + UnknownRole ) const ( - FCGI_MAX_CONNS string = "MAX_CONNS" - FCGI_MAX_REQS string = "MAX_REQS" - FCGI_MPXS_CONNS string = "MPXS_CONNS" + MaxConns string = "MAX_CONNS" + MaxRequests string = "MAX_REQS" + MultiplexConns string = "MPXS_CONNS" ) const ( @@ -79,7 +79,7 @@ const ( type header struct { Version uint8 Type uint8 - Id uint16 + ID uint16 ContentLength uint16 PaddingLength uint8 Reserved uint8 @@ -92,7 +92,7 @@ var pad [maxPad]byte func (h *header) init(recType uint8, reqID uint16, contentLength int) { h.Version = 1 h.Type = recType - h.Id = reqID + h.ID = reqID h.ContentLength = uint16(contentLength) h.PaddingLength = uint8(-contentLength & 7) } @@ -110,7 +110,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) { err = errors.New("fcgi: invalid header version") return } - if rec.h.Type == FCGI_END_REQUEST { + if rec.h.Type == EndRequest { err = io.EOF return } @@ -126,13 +126,15 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) { return } +// FCGIClient implements a FastCGI client, which is a standard for +// interfacing external applications with Web servers. type FCGIClient struct { mutex sync.Mutex rwc io.ReadWriteCloser h header buf bytes.Buffer keepAlive bool - reqId uint16 + reqID uint16 } // Dial connects to the fcgi responder at the specified network address. @@ -148,7 +150,7 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) { fcgi = &FCGIClient{ rwc: conn, keepAlive: false, - reqId: 1, + reqID: 1, } return @@ -163,7 +165,7 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) { c.mutex.Lock() defer c.mutex.Unlock() c.buf.Reset() - c.h.init(recType, c.reqId, len(content)) + c.h.init(recType, c.reqID, len(content)) if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil { return err } @@ -179,14 +181,14 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) { func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error { b := [8]byte{byte(role >> 8), byte(role), flags} - return c.writeRecord(FCGI_BEGIN_REQUEST, b[:]) + return c.writeRecord(BeginRequest, b[:]) } func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error { b := make([]byte, 8) binary.BigEndian.PutUint32(b, uint32(appStatus)) b[4] = protocolStatus - return c.writeRecord(FCGI_END_REQUEST, b) + return c.writeRecord(EndRequest, b) } func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error { @@ -334,17 +336,17 @@ func (w *streamReader) Read(p []byte) (n int, err error) { // Do made the request and returns a io.Reader that translates the data read // from fcgi responder out of fcgi packet before returning it. func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { - err = c.writeBeginRequest(uint16(FCGI_RESPONDER), 0) + err = c.writeBeginRequest(uint16(Responder), 0) if err != nil { return } - err = c.writePairs(FCGI_PARAMS, p) + err = c.writePairs(Params, p) if err != nil { return } - body := newWriter(c, FCGI_STDIN) + body := newWriter(c, Stdin) if req != nil { io.Copy(body, req) } @@ -381,9 +383,9 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res return } if len(statusParts) > 1 { - resp.Status = statusParts[1] + resp.Status = statusParts[1] } - + } else { resp.StatusCode = http.StatusOK } diff --git a/middleware/fastcgi/fcgiclient_test.go b/middleware/fastcgi/fcgiclient_test.go index f843548e..1abaf10f 100644 --- a/middleware/fastcgi/fcgiclient_test.go +++ b/middleware/fastcgi/fcgiclient_test.go @@ -34,13 +34,13 @@ import ( // test failed if the remote fcgi(script) failed md5 verification // and output "FAILED" in response const ( - script_file = "/tank/www/fcgic_test.php" - //ip_port = "remote-php-serv:59000" - ip_port = "127.0.0.1:59000" + scriptFile = "/tank/www/fcgic_test.php" + //ipPort = "remote-php-serv:59000" + ipPort = "127.0.0.1:59000" ) var ( - t_ *testing.T = nil + t_ *testing.T ) type FastCGIServer struct{} @@ -51,7 +51,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { stat := "PASSED" fmt.Fprintln(resp, "-") - file_num := 0 + fileNum := 0 { length := 0 for k0, v0 := range req.Form { @@ -69,7 +69,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { } } if req.MultipartForm != nil { - file_num = len(req.MultipartForm.File) + fileNum = len(req.MultipartForm.File) for kn, fns := range req.MultipartForm.File { //fmt.Fprintln(resp, "server:filekey ", kn ) length += len(kn) @@ -101,11 +101,11 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { fmt.Fprintln(resp, "server:got data length", length) } - fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", file_num, ")--") + fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--") } -func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) { - fcgi, err := Dial("tcp", ip_port) +func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) { + fcgi, err := Dial("tcp", ipPort) if err != nil { log.Println("err:", err) return @@ -119,16 +119,16 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map if len(data) > 0 { length = len(data) rd := bytes.NewReader(data) - resp, err = fcgi.Post(fcgi_params, "", rd, rd.Len()) + resp, err = fcgi.Post(fcgiParams, "", rd, rd.Len()) } else if len(posts) > 0 { values := url.Values{} for k, v := range posts { values.Set(k, v) length += len(k) + 2 + len(v) } - resp, err = fcgi.PostForm(fcgi_params, values) + resp, err = fcgi.PostForm(fcgiParams, values) } else { - resp, err = fcgi.Get(fcgi_params) + resp, err = fcgi.Get(fcgiParams) } default: @@ -142,7 +142,7 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map fi, _ := os.Lstat(v) length += len(k) + int(fi.Size()) } - resp, err = fcgi.PostFile(fcgi_params, values, files) + resp, err = fcgi.PostFile(fcgiParams, values, files) } if err != nil { @@ -191,7 +191,7 @@ func generateRandFile(size int) (p string, m string) { return } -func Disabled_Test(t *testing.T) { +func DisabledTest(t *testing.T) { // TODO: test chunked reader t_ = t @@ -199,7 +199,7 @@ func Disabled_Test(t *testing.T) { // server go func() { - listener, err := net.Listen("tcp", ip_port) + listener, err := net.Listen("tcp", ipPort) if err != nil { // handle error log.Println("listener creatation failed: ", err) @@ -212,19 +212,19 @@ func Disabled_Test(t *testing.T) { time.Sleep(1 * time.Second) // init - fcgi_params := make(map[string]string) - fcgi_params["REQUEST_METHOD"] = "GET" - fcgi_params["SERVER_PROTOCOL"] = "HTTP/1.1" + fcgiParams := make(map[string]string) + fcgiParams["REQUEST_METHOD"] = "GET" + fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1" //fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1" - fcgi_params["SCRIPT_FILENAME"] = script_file + fcgiParams["SCRIPT_FILENAME"] = scriptFile // simple GET log.Println("test:", "get") - sendFcgi(0, fcgi_params, nil, nil, nil) + sendFcgi(0, fcgiParams, nil, nil, nil) // simple post data log.Println("test:", "post") - sendFcgi(0, fcgi_params, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil) + sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil) log.Println("test:", "post data (more than 60KB)") data := "" @@ -240,13 +240,13 @@ func Disabled_Test(t *testing.T) { data += k0 + "=" + url.QueryEscape(v0) + "&" } - sendFcgi(0, fcgi_params, []byte(data), nil, nil) + sendFcgi(0, fcgiParams, []byte(data), nil, nil) log.Println("test:", "post form (use url.Values)") p0 := make(map[string]string, 1) p0["c4ca4238a0b923820dcc509a6f75849b"] = "1" p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n" - sendFcgi(1, fcgi_params, nil, p0, nil) + sendFcgi(1, fcgiParams, nil, p0, nil) log.Println("test:", "post forms (256 keys, more than 1MB)") p1 := make(map[string]string, 1) @@ -257,25 +257,25 @@ func Disabled_Test(t *testing.T) { k0 := fmt.Sprintf("%x", h.Sum(nil)) p1[k0] = v0 } - sendFcgi(1, fcgi_params, nil, p1, nil) + sendFcgi(1, fcgiParams, nil, p1, nil) log.Println("test:", "post file (1 file, 500KB)) ") f0 := make(map[string]string, 1) path0, m0 := generateRandFile(500000) f0[m0] = path0 - sendFcgi(1, fcgi_params, nil, p1, f0) + sendFcgi(1, fcgiParams, nil, p1, f0) log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data") path1, m1 := generateRandFile(5000000) f0[m1] = path1 - sendFcgi(1, fcgi_params, nil, p1, f0) + sendFcgi(1, fcgiParams, nil, p1, f0) log.Println("test:", "post only files (2 files, 5M each)") - sendFcgi(1, fcgi_params, nil, nil, f0) + sendFcgi(1, fcgiParams, nil, nil, f0) log.Println("test:", "post only 1 file") delete(f0, "m0") - sendFcgi(1, fcgi_params, nil, nil, f0) + sendFcgi(1, fcgiParams, nil, nil, f0) os.Remove(path0) os.Remove(path1) diff --git a/middleware/gzip/filter.go b/middleware/gzip/filter.go index ceac660e..7871112f 100644 --- a/middleware/gzip/filter.go +++ b/middleware/gzip/filter.go @@ -81,7 +81,7 @@ func (s Set) Contains(value string) bool { // elements in the set and passes each to f. It returns true // on the first call to f that returns true and false otherwise. func (s Set) ContainsFunc(f func(string) bool) bool { - for k, _ := range s { + for k := range s { if f(k) { return true } diff --git a/middleware/gzip/gzip_test.go b/middleware/gzip/gzip_test.go index 8798f4c5..8d49e8e2 100644 --- a/middleware/gzip/gzip_test.go +++ b/middleware/gzip/gzip_test.go @@ -21,7 +21,7 @@ func TestGzipHandler(t *testing.T) { extFilter.Exts.Add(e) } gz := Gzip{Configs: []Config{ - Config{Filters: []Filter{pathFilter, extFilter}}, + {Filters: []Filter{pathFilter, extFilter}}, }} w := httptest.NewRecorder() diff --git a/middleware/markdown/generator.go b/middleware/markdown/generator.go index c1dfd67a..a02cb3b0 100644 --- a/middleware/markdown/generator.go +++ b/middleware/markdown/generator.go @@ -70,7 +70,7 @@ func generateLinks(md Markdown, cfg *Config) (bool, error) { return generated, g.lastErr } -// generateStaticFiles generates static html files from markdowns. +// generateStaticHTML generates static HTML files from markdowns. func generateStaticHTML(md Markdown, cfg *Config) error { // If generated site already exists, clear it out _, err := os.Stat(cfg.StaticDir) @@ -98,6 +98,7 @@ func generateStaticHTML(md Markdown, cfg *Config) error { if err != nil { return err } + reqPath = filepath.ToSlash(reqPath) reqPath = "/" + reqPath // Generate the static file diff --git a/middleware/markdown/markdown_test.go b/middleware/markdown/markdown_test.go index 6012d0df..8cb89fae 100644 --- a/middleware/markdown/markdown_test.go +++ b/middleware/markdown/markdown_test.go @@ -22,7 +22,7 @@ func TestMarkdown(t *testing.T) { Root: "./testdata", FileSys: http.Dir("./testdata"), Configs: []*Config{ - &Config{ + { Renderer: blackfriday.HtmlRenderer(0, "", ""), PathScope: "/blog", Extensions: []string{".md"}, @@ -32,7 +32,7 @@ func TestMarkdown(t *testing.T) { StaticDir: DefaultStaticDir, StaticFiles: make(map[string]string), }, - &Config{ + { Renderer: blackfriday.HtmlRenderer(0, "", ""), PathScope: "/log", Extensions: []string{".md"}, @@ -42,7 +42,7 @@ func TestMarkdown(t *testing.T) { StaticDir: DefaultStaticDir, StaticFiles: make(map[string]string), }, - &Config{ + { Renderer: blackfriday.HtmlRenderer(0, "", ""), PathScope: "/og", Extensions: []string{".md"}, @@ -52,7 +52,7 @@ func TestMarkdown(t *testing.T) { StaticDir: "testdata/og_static", StaticFiles: map[string]string{"/og/first.md": "testdata/og_static/og/first.md/index.html"}, Links: []PageLink{ - PageLink{ + { Title: "first", Summary: "", Date: time.Now(), diff --git a/middleware/markdown/page.go b/middleware/markdown/page.go index 2b7816ba..3185d8ec 100644 --- a/middleware/markdown/page.go +++ b/middleware/markdown/page.go @@ -116,7 +116,7 @@ func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool { if err != nil { return err } - reqPath = "/" + reqPath + reqPath = "/" + filepath.ToSlash(reqPath) parser := findParser(body) if parser == nil { diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index b7f64404..0fb48dba 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -18,7 +18,7 @@ const ( DefaultStaticDir = "generated_site" ) -type MarkdownData struct { +type Data struct { middleware.Context Doc map[string]string Links []PageLink @@ -95,7 +95,7 @@ func (md Markdown) processTemplate(c *Config, requestPath string, tmpl []byte, m if err != nil { return nil, err } - mdData := MarkdownData{ + mdData := Data{ Context: ctx, Doc: metadata.Variables, Links: c.Links, @@ -134,7 +134,10 @@ func (md Markdown) generatePage(c *Config, requestPath string, content []byte) e } } - filePath := filepath.Join(c.StaticDir, requestPath) + // the URL will always use "/" as a path separator, + // convert that to a native path to support OS that + // use different path separators + filePath := filepath.Join(c.StaticDir, filepath.FromSlash(requestPath)) // If it is index file, use the directory instead if md.IsIndexFile(filepath.Base(requestPath)) { @@ -154,7 +157,7 @@ func (md Markdown) generatePage(c *Config, requestPath string, content []byte) e } c.Lock() - c.StaticFiles[requestPath] = filePath + c.StaticFiles[requestPath] = filepath.ToSlash(filePath) c.Unlock() } diff --git a/middleware/markdown/watcher.go b/middleware/markdown/watcher.go index 76ef2ed5..0b39c741 100644 --- a/middleware/markdown/watcher.go +++ b/middleware/markdown/watcher.go @@ -5,6 +5,8 @@ import ( "time" ) +// DefaultInterval is the default interval at which the markdown watcher +// checks for changes. const DefaultInterval = time.Second * 60 // Watch monitors the configured markdown directory for changes. It calls GenerateLinks diff --git a/middleware/middleware.go b/middleware/middleware.go index 0310ba2f..ba7699ce 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -3,7 +3,7 @@ package middleware import ( "net/http" - "path/filepath" + "path" ) type ( @@ -57,12 +57,19 @@ func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, err // and false is returned. fpath must end in a forward slash '/' // otherwise no index files will be tried (directory paths must end // in a forward slash according to HTTP). +// +// All paths passed into and returned from this function use '/' as the +// path separator, just like URLs. IndexFle handles path manipulation +// internally for systems that use different path separators. func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) { if fpath[len(fpath)-1] != '/' || root == nil { return "", false } for _, indexFile := range indexFiles { - fp := filepath.Join(fpath, indexFile) + // func (http.FileSystem).Open wants all paths separated by "/", + // regardless of operating system convention, so use + // path.Join instead of filepath.Join + fp := path.Join(fpath, indexFile) f, err := root.Open(fp) if err == nil { f.Close() diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go index e5b238e6..700beed8 100644 --- a/middleware/middleware_test.go +++ b/middleware/middleware_test.go @@ -15,9 +15,12 @@ func TestIndexfile(t *testing.T) { expectedBoolValue bool //return value }{ { - http.Dir("./templates/testdata"), "/images/", []string{"img.htm"}, + http.Dir("./templates/testdata"), + "/images/", + []string{"img.htm"}, false, - "/images/img.htm", true, + "/images/img.htm", + true, }, } for i, test := range tests { diff --git a/middleware/mime/mime.go b/middleware/mime/mime.go new file mode 100644 index 00000000..cde699ff --- /dev/null +++ b/middleware/mime/mime.go @@ -0,0 +1,41 @@ +package mime + +import ( + "net/http" + "path/filepath" + + "github.com/mholt/caddy/middleware" +) + +// Config represent a mime config. +type Config struct { + Ext string + ContentType string +} + +// SetContent sets the Content-Type header of the request if the request path +// is supported. +func (c Config) SetContent(w http.ResponseWriter, r *http.Request) bool { + ext := filepath.Ext(r.URL.Path) + if ext != c.Ext { + return false + } + w.Header().Set("Content-Type", c.ContentType) + return true +} + +// Mime sets Content-Type header of requests based on configurations. +type Mime struct { + Next middleware.Handler + Configs []Config +} + +// ServeHTTP implements the middleware.Handler interface. +func (e Mime) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for _, c := range e.Configs { + if ok := c.SetContent(w, r); ok { + break + } + } + return e.Next.ServeHTTP(w, r) +} diff --git a/middleware/mime/mime_test.go b/middleware/mime/mime_test.go new file mode 100644 index 00000000..e7023303 --- /dev/null +++ b/middleware/mime/mime_test.go @@ -0,0 +1,75 @@ +package mime + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mholt/caddy/middleware" +) + +func TestMimeHandler(t *testing.T) { + + mimes := map[string]string{ + ".html": "text/html", + ".txt": "text/plain", + ".swf": "application/x-shockwave-flash", + } + + var configs []Config + for ext, contentType := range mimes { + configs = append(configs, Config{Ext: ext, ContentType: contentType}) + } + + m := Mime{Configs: configs} + + w := httptest.NewRecorder() + exts := []string{ + ".html", ".txt", ".swf", + } + for _, e := range exts { + url := "/file" + e + r, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Error(err) + } + m.Next = nextFunc(true, mimes[e]) + _, err = m.ServeHTTP(w, r) + if err != nil { + t.Error(err) + } + } + + w = httptest.NewRecorder() + exts = []string{ + ".htm1", ".abc", ".mdx", + } + for _, e := range exts { + url := "/file" + e + r, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Error(err) + } + m.Next = nextFunc(false, "") + _, err = m.ServeHTTP(w, r) + if err != nil { + t.Error(err) + } + } +} + +func nextFunc(shouldMime bool, contentType string) middleware.Handler { + return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { + if shouldMime { + if w.Header().Get("Content-Type") != contentType { + return 0, fmt.Errorf("expected Content-Type: %v, found %v", contentType, r.Header.Get("Content-Type")) + } + return 0, nil + } + if w.Header().Get("Content-Type") != "" { + return 0, fmt.Errorf("Content-Type header not expected") + } + return 0, nil + }) +} diff --git a/middleware/proxy/policy_test.go b/middleware/proxy/policy_test.go index d4c75225..b7d63327 100644 --- a/middleware/proxy/policy_test.go +++ b/middleware/proxy/policy_test.go @@ -12,13 +12,13 @@ func (r *customPolicy) Select(pool HostPool) *UpstreamHost { func testPool() HostPool { pool := []*UpstreamHost{ - &UpstreamHost{ + { Name: "http://google.com", // this should resolve (healthcheck test) }, - &UpstreamHost{ + { Name: "http://shouldnot.resolve", // this shouldn't }, - &UpstreamHost{ + { Name: "http://C", }, } diff --git a/middleware/proxy/upstream.go b/middleware/proxy/upstream.go index 9a2a0104..3ab8aa9b 100644 --- a/middleware/proxy/upstream.go +++ b/middleware/proxy/upstream.go @@ -13,8 +13,8 @@ import ( ) var ( - supportedPolicies map[string]func() Policy = make(map[string]func() Policy) - proxyHeaders http.Header = make(http.Header) + supportedPolicies = make(map[string]func() Policy) + proxyHeaders = make(http.Header) ) type staticUpstream struct { @@ -53,64 +53,8 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) { } for c.NextBlock() { - switch c.Val() { - case "policy": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - - if policyCreateFunc, ok := supportedPolicies[c.Val()]; ok { - upstream.Policy = policyCreateFunc() - } else { - return upstreams, c.ArgErr() - } - case "fail_timeout": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - if dur, err := time.ParseDuration(c.Val()); err == nil { - upstream.FailTimeout = dur - } else { - return upstreams, err - } - case "max_fails": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - if n, err := strconv.Atoi(c.Val()); err == nil { - upstream.MaxFails = int32(n) - } else { - return upstreams, err - } - case "health_check": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - upstream.HealthCheck.Path = c.Val() - upstream.HealthCheck.Interval = 30 * time.Second - if c.NextArg() { - if dur, err := time.ParseDuration(c.Val()); err == nil { - upstream.HealthCheck.Interval = dur - } else { - return upstreams, err - } - } - case "proxy_header": - var header, value string - if !c.Args(&header, &value) { - return upstreams, c.ArgErr() - } - proxyHeaders.Add(header, value) - case "websocket": - proxyHeaders.Add("Connection", "{>Connection}") - proxyHeaders.Add("Upgrade", "{>Upgrade}") - case "without": - if !c.NextArg() { - return upstreams, c.ArgErr() - } - upstream.WithoutPathPrefix = c.Val() - default: - return upstreams, c.Errf("unknown property '%s'", c.Val()) + if err := parseBlock(&c, upstream); err != nil { + return upstreams, err } } @@ -165,6 +109,68 @@ func (u *staticUpstream) From() string { return u.from } +func parseBlock(c *parse.Dispenser, u *staticUpstream) error { + switch c.Val() { + case "policy": + if !c.NextArg() { + return c.ArgErr() + } + policyCreateFunc, ok := supportedPolicies[c.Val()] + if !ok { + return c.ArgErr() + } + u.Policy = policyCreateFunc() + case "fail_timeout": + if !c.NextArg() { + return c.ArgErr() + } + dur, err := time.ParseDuration(c.Val()) + if err != nil { + return err + } + u.FailTimeout = dur + case "max_fails": + if !c.NextArg() { + return c.ArgErr() + } + n, err := strconv.Atoi(c.Val()) + if err != nil { + return err + } + u.MaxFails = int32(n) + case "health_check": + if !c.NextArg() { + return c.ArgErr() + } + u.HealthCheck.Path = c.Val() + u.HealthCheck.Interval = 30 * time.Second + if c.NextArg() { + dur, err := time.ParseDuration(c.Val()) + if err != nil { + return err + } + u.HealthCheck.Interval = dur + } + case "proxy_header": + var header, value string + if !c.Args(&header, &value) { + return c.ArgErr() + } + proxyHeaders.Add(header, value) + case "websocket": + proxyHeaders.Add("Connection", "{>Connection}") + proxyHeaders.Add("Upgrade", "{>Upgrade}") + case "without": + if !c.NextArg() { + return c.ArgErr() + } + u.WithoutPathPrefix = c.Val() + default: + return c.Errf("unknown property '%s'", c.Val()) + } + return nil +} + func (u *staticUpstream) healthCheck() { for _, host := range u.Hosts { hostURL := host.Name + u.HealthCheck.Path diff --git a/middleware/redirect/redirect.go b/middleware/redirect/redirect.go index 6f9ab35e..04fb1c63 100644 --- a/middleware/redirect/redirect.go +++ b/middleware/redirect/redirect.go @@ -19,7 +19,7 @@ type Redirect struct { // ServeHTTP implements the middleware.Handler interface. func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range rd.Rules { - if rule.From == "/" || r.URL.Path == rule.From { + if (rule.FromPath == "/" || r.URL.Path == rule.FromPath) && schemeMatches(rule, r) { to := middleware.NewReplacer(r, nil, "").Replace(rule.To) if rule.Meta { safeTo := html.EscapeString(to) @@ -33,11 +33,16 @@ func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error return rd.Next.ServeHTTP(w, r) } +func schemeMatches(rule Rule, req *http.Request) bool { + return (rule.FromScheme == "https" && req.TLS != nil) || + (rule.FromScheme != "https" && req.TLS == nil) +} + // Rule describes an HTTP redirect rule. type Rule struct { - From, To string - Code int - Meta bool + FromScheme, FromPath, To string + Code int + Meta bool } // Script tag comes first since that will better imitate a redirect in the browser's diff --git a/middleware/redirect/redirect_test.go b/middleware/redirect/redirect_test.go index fd98b33a..3107921a 100644 --- a/middleware/redirect/redirect_test.go +++ b/middleware/redirect/redirect_test.go @@ -2,9 +2,11 @@ package redirect import ( "bytes" + "crypto/tls" "io/ioutil" "net/http" "net/http/httptest" + "strings" "testing" "github.com/mholt/caddy/middleware" @@ -14,15 +16,28 @@ func TestRedirect(t *testing.T) { for i, test := range []struct { from string expectedLocation string + expectedCode int }{ - {"/from", "/to"}, - {"/a", "/b"}, - {"/aa", ""}, - {"/", ""}, - {"/a?foo=bar", "/b"}, - {"/asdf?foo=bar", ""}, - {"/foo#bar", ""}, - {"/a#foo", "/b"}, + {"http://localhost/from", "/to", http.StatusMovedPermanently}, + {"http://localhost/a", "/b", http.StatusTemporaryRedirect}, + {"http://localhost/aa", "", http.StatusOK}, + {"http://localhost/", "", http.StatusOK}, + {"http://localhost/a?foo=bar", "/b", http.StatusTemporaryRedirect}, + {"http://localhost/asdf?foo=bar", "", http.StatusOK}, + {"http://localhost/foo#bar", "", http.StatusOK}, + {"http://localhost/a#foo", "/b", http.StatusTemporaryRedirect}, + + // The scheme checks that were added to this package don't actually + // help with redirects because of Caddy's design: a redirect middleware + // for http will always be different than the redirect middleware for + // https because they have to be on different listeners. These tests + // just go to show extra bulletproofing, I guess. + {"http://localhost/scheme", "https://localhost/scheme", http.StatusMovedPermanently}, + {"https://localhost/scheme", "", http.StatusOK}, + {"https://localhost/scheme2", "http://localhost/scheme2", http.StatusMovedPermanently}, + {"http://localhost/scheme2", "", http.StatusOK}, + {"http://localhost/scheme3", "https://localhost/scheme3", http.StatusMovedPermanently}, + {"https://localhost/scheme3", "", http.StatusOK}, } { var nextCalled bool @@ -32,8 +47,16 @@ func TestRedirect(t *testing.T) { return 0, nil }), Rules: []Rule{ - {From: "/from", To: "/to"}, - {From: "/a", To: "/b"}, + {FromPath: "/from", To: "/to", Code: http.StatusMovedPermanently}, + {FromPath: "/a", To: "/b", Code: http.StatusTemporaryRedirect}, + + // These http and https schemes would never actually be mixed in the same + // redirect rule with Caddy because http and https schemes have different listeners, + // so they don't share a redirect rule. So although these tests prove something + // impossible with Caddy, it's extra bulletproofing at very little cost. + {FromScheme: "http", FromPath: "/scheme", To: "https://localhost/scheme", Code: http.StatusMovedPermanently}, + {FromScheme: "https", FromPath: "/scheme2", To: "http://localhost/scheme2", Code: http.StatusMovedPermanently}, + {FromScheme: "", FromPath: "/scheme3", To: "https://localhost/scheme3", Code: http.StatusMovedPermanently}, }, } @@ -41,6 +64,9 @@ func TestRedirect(t *testing.T) { if err != nil { t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) } + if strings.HasPrefix(test.from, "https://") { + req.TLS = new(tls.ConnectionState) // faux HTTPS + } rec := httptest.NewRecorder() re.ServeHTTP(rec, req) @@ -50,6 +76,11 @@ func TestRedirect(t *testing.T) { i, test.expectedLocation, rec.Header().Get("Location")) } + if rec.Code != test.expectedCode { + t.Errorf("Test %d: Expected status code to be %d but was %d", + i, test.expectedCode, rec.Code) + } + if nextCalled && test.expectedLocation != "" { t.Errorf("Test %d: Next handler was unexpectedly called", i) } @@ -59,7 +90,7 @@ func TestRedirect(t *testing.T) { func TestParametersRedirect(t *testing.T) { re := Redirect{ Rules: []Rule{ - {From: "/", Meta: false, To: "http://example.com{uri}"}, + {FromPath: "/", Meta: false, To: "http://example.com{uri}"}, }, } @@ -77,7 +108,7 @@ func TestParametersRedirect(t *testing.T) { re = Redirect{ Rules: []Rule{ - {From: "/", Meta: false, To: "http://example.com/a{path}?b=c&{query}"}, + {FromPath: "/", Meta: false, To: "http://example.com/a{path}?b=c&{query}"}, }, } @@ -96,13 +127,13 @@ func TestParametersRedirect(t *testing.T) { func TestMetaRedirect(t *testing.T) { re := Redirect{ Rules: []Rule{ - {From: "/whatever", Meta: true, To: "/something"}, - {From: "/", Meta: true, To: "https://example.com/"}, + {FromPath: "/whatever", Meta: true, To: "/something"}, + {FromPath: "/", Meta: true, To: "https://example.com/"}, }, } for i, test := range re.Rules { - req, err := http.NewRequest("GET", test.From, nil) + req, err := http.NewRequest("GET", test.FromPath, nil) if err != nil { t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) } diff --git a/middleware/replacer_test.go b/middleware/replacer_test.go index 7986a735..39d91a22 100644 --- a/middleware/replacer_test.go +++ b/middleware/replacer_test.go @@ -10,9 +10,9 @@ import ( func TestNewReplacer(t *testing.T) { w := httptest.NewRecorder() recordRequest := NewResponseRecorder(w) - userJson := `{"username": "dennis"}` + userJSON := `{"username": "dennis"}` - reader := strings.NewReader(userJson) //Convert string to reader + reader := strings.NewReader(userJSON) //Convert string to reader request, err := http.NewRequest("POST", "http://caddyserver.com", reader) //Create request with JSON body if err != nil { @@ -41,9 +41,9 @@ func TestNewReplacer(t *testing.T) { func TestReplace(t *testing.T) { w := httptest.NewRecorder() recordRequest := NewResponseRecorder(w) - userJson := `{"username": "dennis"}` + userJSON := `{"username": "dennis"}` - reader := strings.NewReader(userJson) //Convert string to reader + reader := strings.NewReader(userJSON) //Convert string to reader request, err := http.NewRequest("POST", "http://caddyserver.com", reader) //Create request with JSON body if err != nil { diff --git a/middleware/rewrite/rewrite.go b/middleware/rewrite/rewrite.go index ed2e9bd9..885bfad2 100644 --- a/middleware/rewrite/rewrite.go +++ b/middleware/rewrite/rewrite.go @@ -49,6 +49,9 @@ func NewSimpleRule(from, to string) SimpleRule { // Rewrite rewrites the internal location of the current request. func (s SimpleRule) Rewrite(r *http.Request) bool { if s.From == r.URL.Path { + // take note of this rewrite for internal use by fastcgi + // all we need is the URI, not full URL + r.Header.Set(headerFieldName, r.URL.RequestURI()) r.URL.Path = s.To return true } @@ -112,7 +115,7 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool { // include trailing slash in regexp if present start := len(r.Base) if strings.HasSuffix(r.Base, "/") { - start -= 1 + start-- } // validate regexp @@ -129,6 +132,10 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool { return false } + // take note of this rewrite for internal use by fastcgi + // all we need is the URI, not full URL + req.Header.Set(headerFieldName, req.URL.RequestURI()) + // perform rewrite req.URL.Path = url.Path if url.RawQuery != "" { @@ -169,3 +176,8 @@ func (r *RegexpRule) matchExt(rPath string) bool { } return true } + +// When a rewrite is performed, this header is added to the request +// and is for internal use only, specifically the fastcgi middleware. +// It contains the original request URI before the rewrite. +const headerFieldName = "Caddy-Rewrite-Original-URI" diff --git a/middleware/rewrite/rewrite_test.go b/middleware/rewrite/rewrite_test.go index b4845f10..23280aee 100644 --- a/middleware/rewrite/rewrite_test.go +++ b/middleware/rewrite/rewrite_test.go @@ -21,15 +21,15 @@ func TestRewrite(t *testing.T) { } regexpRules := [][]string{ - []string{"/reg/", ".*", "/to", ""}, - []string{"/r/", "[a-z]+", "/toaz", "!.html|"}, - []string{"/url/", "a([a-z0-9]*)s([A-Z]{2})", "/to/{path}", ""}, - []string{"/ab/", "ab", "/ab?{query}", ".txt|"}, - []string{"/ab/", "ab", "/ab?type=html&{query}", ".html|"}, - []string{"/abc/", "ab", "/abc/{file}", ".html|"}, - []string{"/abcd/", "ab", "/a/{dir}/{file}", ".html|"}, - []string{"/abcde/", "ab", "/a#{fragment}", ".html|"}, - []string{"/ab/", `.*\.jpg`, "/ajpg", ""}, + {"/reg/", ".*", "/to", ""}, + {"/r/", "[a-z]+", "/toaz", "!.html|"}, + {"/url/", "a([a-z0-9]*)s([A-Z]{2})", "/to/{path}", ""}, + {"/ab/", "ab", "/ab?{query}", ".txt|"}, + {"/ab/", "ab", "/ab?type=html&{query}", ".html|"}, + {"/abc/", "ab", "/abc/{file}", ".html|"}, + {"/abcd/", "ab", "/a/{dir}/{file}", ".html|"}, + {"/abcde/", "ab", "/a#{fragment}", ".html|"}, + {"/ab/", `.*\.jpg`, "/ajpg", ""}, } for _, regexpRule := range regexpRules { diff --git a/middleware/roller.go b/middleware/roller.go index 0f8a2b2e..995cabf9 100644 --- a/middleware/roller.go +++ b/middleware/roller.go @@ -6,6 +6,7 @@ import ( "gopkg.in/natefinch/lumberjack.v2" ) +// LogRoller implements a middleware that provides a rolling logger. type LogRoller struct { Filename string MaxSize int @@ -14,6 +15,7 @@ type LogRoller struct { LocalTime bool } +// GetLogWriter returns an io.Writer that writes to a rolling logger. func (l LogRoller) GetLogWriter() io.Writer { return &lumberjack.Logger{ Filename: l.Filename, diff --git a/middleware/templates/templates_test.go b/middleware/templates/templates_test.go index 5c283390..3ee6072c 100644 --- a/middleware/templates/templates_test.go +++ b/middleware/templates/templates_test.go @@ -14,12 +14,12 @@ func Test(t *testing.T) { return 0, nil }), Rules: []Rule{ - Rule{ + { Extensions: []string{".html"}, IndexFiles: []string{"index.html"}, Path: "/photos", }, - Rule{ + { Extensions: []string{".html", ".htm"}, IndexFiles: []string{"index.html", "index.htm"}, Path: "/images", @@ -34,7 +34,7 @@ func Test(t *testing.T) { return 0, nil }), Rules: []Rule{ - Rule{ + { Extensions: []string{".html"}, IndexFiles: []string{"index.html"}, Path: "/", diff --git a/middleware/websocket/websocket.go b/middleware/websocket/websocket.go new file mode 100644 index 00000000..f344fe51 --- /dev/null +++ b/middleware/websocket/websocket.go @@ -0,0 +1,229 @@ +// Package websocket implements a WebSocket server by executing +// a command and piping its input and output through the WebSocket +// connection. +package websocket + +import ( + "io" + "net" + "net/http" + "os/exec" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/mholt/caddy/middleware" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 1024 * 1024 * 10 // 10 MB default. +) + +var ( + // GatewayInterface is the dialect of CGI being used by the server + // to communicate with the script. See CGI spec, 4.1.4 + GatewayInterface string + + // ServerSoftware is the name and version of the information server + // software making the CGI request. See CGI spec, 4.1.17 + ServerSoftware string +) + +type ( + // WebSocket is a type that holds configuration for the + // websocket middleware generally, like a list of all the + // websocket endpoints. + WebSocket struct { + // Next is the next HTTP handler in the chain for when the path doesn't match + Next middleware.Handler + + // Sockets holds all the web socket endpoint configurations + Sockets []Config + } + + // Config holds the configuration for a single websocket + // endpoint which may serve multiple websocket connections. + Config struct { + Path string + Command string + Arguments []string + Respawn bool // TODO: Not used, but parser supports it until we decide on it + } +) + +// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up. +func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + for _, sockconfig := range ws.Sockets { + if middleware.Path(r.URL.Path).Matches(sockconfig.Path) { + return serveWS(w, r, &sockconfig) + } + } + + // Didn't match a websocket path, so pass-thru + return ws.Next.ServeHTTP(w, r) +} + +// serveWS is used for setting and upgrading the HTTP connection to a websocket connection. +// It also spawns the child process that is associated with matched HTTP path/url. +func serveWS(w http.ResponseWriter, r *http.Request, config *Config) (int, error) { + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { return true }, + } + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + return http.StatusBadRequest, err + } + defer conn.Close() + + cmd := exec.Command(config.Command, config.Arguments...) + stdout, err := cmd.StdoutPipe() + if err != nil { + return http.StatusBadGateway, err + } + + stdin, err := cmd.StdinPipe() + if err != nil { + return http.StatusBadGateway, err + } + + metavars, err := buildEnv(cmd.Path, r) + if err != nil { + return http.StatusBadGateway, err + } + + cmd.Env = metavars + + if err := cmd.Start(); err != nil { + return http.StatusBadGateway, err + } + + reader(conn, stdout, stdin) + + return 0, nil +} + +// buildEnv creates the meta-variables for the child process according +// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1 +// cmdPath should be the path of the command being run. +// The returned string slice can be set to the command's Env property. +func buildEnv(cmdPath string, r *http.Request) (metavars []string, err error) { + remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return + } + + serverHost, serverPort, err := net.SplitHostPort(r.Host) + if err != nil { + return + } + + metavars = []string{ + `AUTH_TYPE=`, // Not used + `CONTENT_LENGTH=`, // Not used + `CONTENT_TYPE=`, // Not used + `GATEWAY_INTERFACE=` + GatewayInterface, + `PATH_INFO=`, // TODO + `PATH_TRANSLATED=`, // TODO + `QUERY_STRING=` + r.URL.RawQuery, + `REMOTE_ADDR=` + remoteHost, + `REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them + `REMOTE_IDENT=`, // Not used + `REMOTE_PORT=` + remotePort, + `REMOTE_USER=`, // Not used, + `REQUEST_METHOD=` + r.Method, + `REQUEST_URI=` + r.RequestURI, + `SCRIPT_NAME=` + cmdPath, // path of the program being executed + `SERVER_NAME=` + serverHost, + `SERVER_PORT=` + serverPort, + `SERVER_PROTOCOL=` + r.Proto, + `SERVER_SOFTWARE=` + ServerSoftware, + } + + // Add each HTTP header to the environment as well + for header, values := range r.Header { + value := strings.Join(values, ", ") + header = strings.ToUpper(header) + header = strings.Replace(header, "-", "_", -1) + value = strings.Replace(value, "\n", " ", -1) + metavars = append(metavars, "HTTP_"+header+"="+value) + } + + return +} + +// reader is the guts of this package. It takes the stdin and stdout pipes +// of the cmd we created in ServeWS and pipes them between the client and server +// over websockets. +func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) { + // Setup our connection's websocket ping/pong handlers from our const values. + conn.SetReadLimit(maxMessageSize) + conn.SetReadDeadline(time.Now().Add(pongWait)) + conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + tickerChan := make(chan bool) + defer func() { tickerChan <- true }() // make sure to close the ticker when we are done. + go ticker(conn, tickerChan) + + for { + msgType, r, err := conn.NextReader() + if err != nil { + if msgType == -1 { + return // we got a disconnect from the client. We are good to close. + } + conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{}) + return + } + + w, err := conn.NextWriter(msgType) + if err != nil { + conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{}) + return + } + + if _, err := io.Copy(stdin, r); err != nil { + conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{}) + return + } + + go func() { + if _, err := io.Copy(w, stdout); err != nil { + conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{}) + return + } + if err := w.Close(); err != nil { + conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{}) + return + } + }() + } +} + +// ticker is start by the reader. Basically it is the method that simulates the websocket +// between the server and client to keep it alive with ping messages. +func ticker(conn *websocket.Conn, c chan bool) { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + close(c) + }() + + for { // blocking loop with select to wait for stimulation. + select { + case <-ticker.C: + conn.WriteMessage(websocket.PingMessage, nil) + case <-c: + return // clean up this routine. + } + } +} diff --git a/middleware/websockets/websocket.go b/middleware/websockets/websocket.go deleted file mode 100644 index 4c843f05..00000000 --- a/middleware/websockets/websocket.go +++ /dev/null @@ -1,89 +0,0 @@ -package websockets - -import ( - "net" - "net/http" - "os/exec" - "strings" - - "golang.org/x/net/websocket" -) - -// WebSocket represents a web socket server instance. A WebSocket -// is instantiated for each new websocket request/connection. -type WebSocket struct { - Config - *http.Request -} - -// Handle handles a WebSocket connection. It launches the -// specified command and streams input and output through -// the command's stdin and stdout. -func (ws WebSocket) Handle(conn *websocket.Conn) { - cmd := exec.Command(ws.Command, ws.Arguments...) - - cmd.Stdin = conn - cmd.Stdout = conn - cmd.Stderr = conn // TODO: Make this configurable from the Caddyfile - - metavars, err := ws.buildEnv(cmd.Path) - if err != nil { - panic(err) // TODO - } - - cmd.Env = metavars - - err = cmd.Run() - if err != nil { - panic(err) - } -} - -// buildEnv creates the meta-variables for the child process according -// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1 -// cmdPath should be the path of the command being run. -// The returned string slice can be set to the command's Env property. -func (ws WebSocket) buildEnv(cmdPath string) (metavars []string, err error) { - remoteHost, remotePort, err := net.SplitHostPort(ws.RemoteAddr) - if err != nil { - return - } - - serverHost, serverPort, err := net.SplitHostPort(ws.Host) - if err != nil { - return - } - - metavars = []string{ - `AUTH_TYPE=`, // Not used - `CONTENT_LENGTH=`, // Not used - `CONTENT_TYPE=`, // Not used - `GATEWAY_INTERFACE=` + GatewayInterface, - `PATH_INFO=`, // TODO - `PATH_TRANSLATED=`, // TODO - `QUERY_STRING=` + ws.URL.RawQuery, - `REMOTE_ADDR=` + remoteHost, - `REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them - `REMOTE_IDENT=`, // Not used - `REMOTE_PORT=` + remotePort, - `REMOTE_USER=`, // Not used, - `REQUEST_METHOD=` + ws.Method, - `REQUEST_URI=` + ws.RequestURI, - `SCRIPT_NAME=` + cmdPath, // path of the program being executed - `SERVER_NAME=` + serverHost, - `SERVER_PORT=` + serverPort, - `SERVER_PROTOCOL=` + ws.Proto, - `SERVER_SOFTWARE=` + ServerSoftware, - } - - // Add each HTTP header to the environment as well - for header, values := range ws.Header { - value := strings.Join(values, ", ") - header = strings.ToUpper(header) - header = strings.Replace(header, "-", "_", -1) - value = strings.Replace(value, "\n", " ", -1) - metavars = append(metavars, "HTTP_"+header+"="+value) - } - - return -} diff --git a/middleware/websockets/websockets.go b/middleware/websockets/websockets.go deleted file mode 100644 index 81e40510..00000000 --- a/middleware/websockets/websockets.go +++ /dev/null @@ -1,60 +0,0 @@ -// Package websockets implements a WebSocket server by executing -// a command and piping its input and output through the WebSocket -// connection. -package websockets - -import ( - "net/http" - - "github.com/mholt/caddy/middleware" - "golang.org/x/net/websocket" -) - -type ( - // WebSockets is a type that holds configuration for the - // websocket middleware generally, like a list of all the - // websocket endpoints. - WebSockets struct { - // Next is the next HTTP handler in the chain for when the path doesn't match - Next middleware.Handler - - // Sockets holds all the web socket endpoint configurations - Sockets []Config - } - - // Config holds the configuration for a single websocket - // endpoint which may serve multiple websocket connections. - Config struct { - Path string - Command string - Arguments []string - Respawn bool // TODO: Not used, but parser supports it until we decide on it - } -) - -// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up. -func (ws WebSockets) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { - for _, sockconfig := range ws.Sockets { - if middleware.Path(r.URL.Path).Matches(sockconfig.Path) { - socket := WebSocket{ - Config: sockconfig, - Request: r, - } - websocket.Handler(socket.Handle).ServeHTTP(w, r) - return 0, nil - } - } - - // Didn't match a websocket path, so pass-thru - return ws.Next.ServeHTTP(w, r) -} - -var ( - // GatewayInterface is the dialect of CGI being used by the server - // to communicate with the script. See CGI spec, 4.1.4 - GatewayInterface string - - // ServerSoftware is the name and version of the information server - // software making the CGI request. See CGI spec, 4.1.17 - ServerSoftware string -) diff --git a/server/server.go b/server/server.go index 03bca607..24aa92eb 100644 --- a/server/server.go +++ b/server/server.go @@ -86,7 +86,7 @@ func (s *Server) Serve() error { go func(vh virtualHost) { // Wait for signal interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, os.Kill) + signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only) <-interrupt // Run callbacks @@ -264,6 +264,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +// DefaultErrorFunc responds to an HTTP request with a simple description +// of the specified HTTP status code. func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) { w.WriteHeader(status) fmt.Fprintf(w, "%d %s", status, http.StatusText(status))