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
+