Commit 0a1e472f authored by Matthew Holt's avatar Matthew Holt

Merge branch 'configfix' into letsencrypt

Conflicts:
	config/config.go
parents 4e92c712 22367801
[![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.
......
......@@ -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
......
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
......@@ -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
......@@ -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)
......@@ -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")
}
......
......@@ -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
......
......@@ -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
}
......@@ -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{}},
{`import not_found.txt`, true, []address{}, map[string]int{}},
{`""`, false, []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 {
dir1
}`, false, 1},
{`http://localhost https://localhost
dir1 foo bar`, false, 1},
{`localhost`, false, [][]address{
{{"localhost", ""}},
}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, 1},
{`localhost:1234`, false, [][]address{
[]address{{"localhost", "1234"}},
}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, 1},
{`localhost:1234 {
}
localhost:2015 {
}`, false, [][]address{
[]address{{"localhost", "1234"}},
[]address{{"localhost", "2015"}},
}},
{`host1 {
}
host2 {
}`, false, 2},
{`localhost:1234, http://host2`, false, [][]address{
[]address{{"localhost", "1234"}, {"host2", "http"}},
}},
{`""`, false, 0},
{`localhost:1234, http://host2,`, true, [][]address{}},
{``, 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": {},
}
}
......
......@@ -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
......
......@@ -193,6 +193,10 @@ th a {
margin-top: 70px;
}
}
.name {
white-space: pre;
}
</style>
</head>
<body>
......@@ -240,7 +244,7 @@ th a {
<tr>
<td>
{{if .IsDir}}&#128194;{{else}}&#128196;{{end}}
<a href="{{.URL}}">{{.Name}}</a>
<a href="{{.URL}}" class="name">{{.Name}}</a>
</td>
<td>{{.HumanSize}}</td>
<td class="hideable">{{.HumanModTime "01/02/2006 3:04:05 PM -0700"}}</td>
......
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)
}
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)
}
......@@ -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())
......
......@@ -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
}
......
......@@ -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")
}
}
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 := `<!DOCTYPE html>
expectedBody := []byte(`<!DOCTYPE html>
<html>
<head>
<title>first_post</title>
......@@ -104,9 +105,10 @@ func TestMarkdownStaticGen(t *testing.T) {
</body>
</html>
`
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)
......
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
}
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)
}
}
}
......@@ -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
}
......@@ -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
}
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")
}
......@@ -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
})
}
......@@ -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,
......
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",
}, {
......
CHANGES
<master>
- 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
......
CADDY 0.7.5
CADDY 0.7.6
Website
https://caddyserver.com
......
......@@ -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 != "" {
......
......@@ -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
......
......@@ -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)
......
......@@ -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()
......
......@@ -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) {
<a href="test2.html">test2.html</a><br>
<a href="test3.html">test3.html</a><br>
</body>
</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 := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
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},
}
listing.Sort = "name"
listing.Order = "asc"
listing.applySort()
marsh, err := json.Marshal(listing.Items)
if err != nil {
t.Fatalf("Unable to Marshal the listing ")
}
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)
}
}
}
<!DOCTYPE html>
<html>
</html>
\ No newline at end of file
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)
}
}
}
}
......@@ -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 {
......
......@@ -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,
}
......
......@@ -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
}
......
......@@ -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)
......
......@@ -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
}
......
......@@ -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()
......
......@@ -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
......
......@@ -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(),
......
......@@ -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 {
......
......@@ -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()
}
......
......@@ -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
......
......@@ -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()
......
......@@ -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 {
......
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)
}
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
})
}
......@@ -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",
},
}
......
......@@ -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
......
......@@ -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
......
......@@ -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)
}
......
......@@ -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 {
......
......@@ -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"
......@@ -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 {
......
......@@ -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,
......
......@@ -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: "/",
......
// 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.
}
}
}
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
}
// 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
)
......@@ -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))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment