mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-09 12:28:49 +03:00
Merge branch 'configfix' into letsencrypt
Conflicts: config/config.go
This commit is contained in:
commit
0a1e472fc2
62 changed files with 1574 additions and 689 deletions
|
@ -1,6 +1,6 @@
|
||||||
[![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com)
|
[![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.
|
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"
|
Name = "Caddy"
|
||||||
|
|
||||||
// Version is the program version
|
// Version is the program version
|
||||||
Version = "0.7.5"
|
Version = "0.7.6"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -35,8 +35,8 @@ var (
|
||||||
// Wg is used to wait for all servers to shut down
|
// Wg is used to wait for all servers to shut down
|
||||||
Wg sync.WaitGroup
|
Wg sync.WaitGroup
|
||||||
|
|
||||||
// Http2 indicates whether HTTP2 is enabled or not
|
// HTTP2 indicates whether HTTP2 is enabled or not
|
||||||
Http2 bool // TODO: temporary flag until http2 is standard
|
HTTP2 bool // TODO: temporary flag until http2 is standard
|
||||||
|
|
||||||
// Quiet mode hides non-error initialization output
|
// Quiet mode hides non-error initialization output
|
||||||
Quiet bool
|
Quiet bool
|
||||||
|
|
20
appveyor.yml
Normal file
20
appveyor.yml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
version: "{build}"
|
||||||
|
|
||||||
|
os: Windows Server 2012 R2
|
||||||
|
|
||||||
|
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: c:\gopath
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get golang.org/x/tools/cmd/vet
|
||||||
|
- echo %PATH%
|
||||||
|
- echo %GOPATH%
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
- go get -d ./...
|
||||||
|
|
||||||
|
build_script:
|
||||||
|
- go vet ./...
|
||||||
|
- go test ./...
|
124
config/config.go
124
config/config.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/mholt/caddy/app"
|
"github.com/mholt/caddy/app"
|
||||||
"github.com/mholt/caddy/config/parse"
|
"github.com/mholt/caddy/config/parse"
|
||||||
|
@ -42,32 +43,59 @@ func Load(filename string, input io.Reader) (Group, error) {
|
||||||
return Default()
|
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,
|
// Iterate each server block and make a config for each one,
|
||||||
// executing the directives that were parsed.
|
// executing the directives that were parsed.
|
||||||
for _, sb := range serverBlocks {
|
for i, sb := range serverBlocks {
|
||||||
sharedConfig, err := serverBlockToConfig(filename, sb)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 == "" {
|
if config.Port == "" {
|
||||||
config.Port = 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)
|
configs = append(configs, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,44 +134,21 @@ func Load(filename string, input io.Reader) (Group, error) {
|
||||||
return arrangeBindings(configs)
|
return arrangeBindings(configs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverBlockToConfig makes a config for the server block
|
// makeOnces makes a map of directive name to sync.Once
|
||||||
// by executing the tokens that were parsed. The returned
|
// instance. This is intended to be called once per server
|
||||||
// config is shared among all hosts/addresses for the server
|
// block when setting up configs so that Setup functions
|
||||||
// block, so Host and Port information is not filled out
|
// for each directive can perform a task just once per
|
||||||
// here.
|
// server block, even if there are multiple hosts on the block.
|
||||||
func serverBlockToConfig(filename string, sb parse.ServerBlock) (server.Config, error) {
|
//
|
||||||
sharedConfig := server.Config{
|
// We need one Once per directive, otherwise the first
|
||||||
Root: Root,
|
// directive to use it would exclude other directives from
|
||||||
Middleware: make(map[string][]middleware.Middleware),
|
// using it at all, which would be a bug.
|
||||||
ConfigFile: filename,
|
func makeOnces() map[string]*sync.Once {
|
||||||
AppName: app.Name,
|
onces := make(map[string]*sync.Once)
|
||||||
AppVersion: app.Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is crucial that directives are executed in the proper order.
|
|
||||||
for _, dir := range directiveOrder {
|
for _, dir := range directiveOrder {
|
||||||
// Execute directive if it is in the server block
|
onces[dir.name] = new(sync.Once)
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
|
return onces
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sharedConfig, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// arrangeBindings groups configurations by their bind address. For example,
|
// 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
|
// 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
|
// server. Use the keys of the returned map to create listeners, and use
|
||||||
// the associated values to set up the virtualhosts.
|
// the associated values to set up the virtualhosts.
|
||||||
func arrangeBindings(allConfigs []server.Config) (Group, error) {
|
func arrangeBindings(allConfigs []server.Config) (map[*net.TCPAddr][]server.Config, error) {
|
||||||
addresses := make(Group)
|
addresses := make(map[*net.TCPAddr][]server.Config)
|
||||||
|
|
||||||
// Group configs by bind address
|
// Group configs by bind address
|
||||||
for _, conf := range allConfigs {
|
for _, conf := range allConfigs {
|
||||||
|
@ -263,6 +268,9 @@ func validDirective(d string) bool {
|
||||||
return false
|
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 {
|
func NewDefault() server.Config {
|
||||||
return server.Config{
|
return server.Config{
|
||||||
Root: Root,
|
Root: Root,
|
||||||
|
@ -271,9 +279,8 @@ func NewDefault() server.Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default makes a default configuration which
|
// Default obtains a default config and arranges
|
||||||
// is empty except for root, host, and port,
|
// bindings so it's ready to use.
|
||||||
// which are essentials for serving the cwd.
|
|
||||||
func Default() (Group, error) {
|
func Default() (Group, error) {
|
||||||
return arrangeBindings([]server.Config{NewDefault()})
|
return arrangeBindings([]server.Config{NewDefault()})
|
||||||
}
|
}
|
||||||
|
@ -296,4 +303,5 @@ var (
|
||||||
LetsEncryptAgree bool
|
LetsEncryptAgree bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Group maps network addresses to their configurations.
|
||||||
type Group map[*net.TCPAddr][]server.Config
|
type Group map[*net.TCPAddr][]server.Config
|
||||||
|
|
|
@ -57,6 +57,7 @@ var directiveOrder = []directive{
|
||||||
{"rewrite", setup.Rewrite},
|
{"rewrite", setup.Rewrite},
|
||||||
{"redir", setup.Redir},
|
{"redir", setup.Redir},
|
||||||
{"ext", setup.Ext},
|
{"ext", setup.Ext},
|
||||||
|
{"mime", setup.Mime},
|
||||||
{"basicauth", setup.BasicAuth},
|
{"basicauth", setup.BasicAuth},
|
||||||
{"internal", setup.Internal},
|
{"internal", setup.Internal},
|
||||||
{"proxy", setup.Proxy},
|
{"proxy", setup.Proxy},
|
||||||
|
@ -73,7 +74,7 @@ type directive struct {
|
||||||
setup SetupFunc
|
setup SetupFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// A setup function takes a setup controller. Its return values may
|
// SetupFunc takes a controller and may optionally return a middleware.
|
||||||
// both be nil. If middleware is not nil, it will be chained into
|
// If the resulting middleware is not nil, it will be chained into
|
||||||
// the HTTP handlers in the order specified in this package.
|
// the HTTP handlers in the order specified in this package.
|
||||||
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
|
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
|
||||||
|
|
|
@ -119,6 +119,7 @@ func (d *Dispenser) NextBlock() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IncrNest adds a level of nesting to the dispenser.
|
||||||
func (d *Dispenser) IncrNest() {
|
func (d *Dispenser) IncrNest() {
|
||||||
d.nesting++
|
d.nesting++
|
||||||
return
|
return
|
||||||
|
@ -208,9 +209,9 @@ func (d *Dispenser) SyntaxErr(expected string) error {
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EofErr returns an EOF error, meaning that end of input
|
// EOFErr returns an error indicating that the dispenser reached
|
||||||
// was found when another token was expected.
|
// the end of the input when searching for the next token.
|
||||||
func (d *Dispenser) EofErr() error {
|
func (d *Dispenser) EOFErr() error {
|
||||||
return d.Errf("Unexpected EOF")
|
return d.Errf("Unexpected EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import "io"
|
||||||
// ServerBlocks parses the input just enough to organize tokens,
|
// ServerBlocks parses the input just enough to organize tokens,
|
||||||
// in order, by server block. No further parsing is performed.
|
// in order, by server block. No further parsing is performed.
|
||||||
// Server blocks are returned in the order in which they appear.
|
// 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)}
|
p := parser{Dispenser: NewDispenser(filename, input)}
|
||||||
blocks, err := p.parseAll()
|
blocks, err := p.parseAll()
|
||||||
return blocks, err
|
return blocks, err
|
||||||
|
|
|
@ -9,12 +9,12 @@ import (
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
Dispenser
|
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
|
eof bool // if we encounter a valid EOF in a hard place
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parseAll() ([]ServerBlock, error) {
|
func (p *parser) parseAll() ([]serverBlock, error) {
|
||||||
var blocks []ServerBlock
|
var blocks []serverBlock
|
||||||
|
|
||||||
for p.Next() {
|
for p.Next() {
|
||||||
err := p.parseOne()
|
err := p.parseOne()
|
||||||
|
@ -30,7 +30,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) parseOne() 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()
|
err := p.begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -87,7 +87,7 @@ func (p *parser) addresses() error {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if tkn != "" {
|
if tkn != "" { // empty token possible if user typed "" in Caddyfile
|
||||||
// Trailing comma indicates another address will follow, which
|
// Trailing comma indicates another address will follow, which
|
||||||
// may possibly be on the next line
|
// may possibly be on the next line
|
||||||
if tkn[len(tkn)-1] == ',' {
|
if tkn[len(tkn)-1] == ',' {
|
||||||
|
@ -102,13 +102,13 @@ func (p *parser) addresses() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// Advance token and possibly break out of loop or return error
|
||||||
hasNext := p.Next()
|
hasNext := p.Next()
|
||||||
if expectingAnother && !hasNext {
|
if expectingAnother && !hasNext {
|
||||||
return p.EofErr()
|
return p.EOFErr()
|
||||||
}
|
}
|
||||||
if !hasNext {
|
if !hasNext {
|
||||||
p.eof = true
|
p.eof = true
|
||||||
|
@ -242,7 +242,7 @@ func (p *parser) directive() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if nesting > 0 {
|
if nesting > 0 {
|
||||||
return p.EofErr()
|
return p.EOFErr()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -301,15 +301,26 @@ func standardAddress(str string) (host, port string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// ServerBlock associates tokens with a list of addresses
|
// serverBlock associates tokens with a list of addresses
|
||||||
// and groups tokens by directive name.
|
// and groups tokens by directive name.
|
||||||
ServerBlock struct {
|
serverBlock struct {
|
||||||
Addresses []Address
|
Addresses []address
|
||||||
Tokens map[string][]token
|
Tokens map[string][]token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Address represents a host and port.
|
address struct {
|
||||||
Address struct {
|
|
||||||
Host, Port string
|
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) {
|
func TestParseOneAndImport(t *testing.T) {
|
||||||
setupParseTests()
|
setupParseTests()
|
||||||
|
|
||||||
testParseOne := func(input string) (ServerBlock, error) {
|
testParseOne := func(input string) (serverBlock, error) {
|
||||||
p := testParser(input)
|
p := testParser(input)
|
||||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||||
err := p.parseOne()
|
err := p.parseOne()
|
||||||
|
@ -69,22 +69,22 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
input string
|
input string
|
||||||
shouldErr bool
|
shouldErr bool
|
||||||
addresses []Address
|
addresses []address
|
||||||
tokens map[string]int // map of directive name to number of tokens expected
|
tokens map[string]int // map of directive name to number of tokens expected
|
||||||
}{
|
}{
|
||||||
{`localhost`, false, []Address{
|
{`localhost`, false, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{}},
|
}, map[string]int{}},
|
||||||
|
|
||||||
{`localhost
|
{`localhost
|
||||||
dir1`, false, []Address{
|
dir1`, false, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 1,
|
"dir1": 1,
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{`localhost:1234
|
{`localhost:1234
|
||||||
dir1 foo bar`, false, []Address{
|
dir1 foo bar`, false, []address{
|
||||||
{"localhost", "1234"},
|
{"localhost", "1234"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 3,
|
"dir1": 3,
|
||||||
|
@ -92,7 +92,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
|
|
||||||
{`localhost {
|
{`localhost {
|
||||||
dir1
|
dir1
|
||||||
}`, false, []Address{
|
}`, false, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 1,
|
"dir1": 1,
|
||||||
|
@ -101,7 +101,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
{`localhost:1234 {
|
{`localhost:1234 {
|
||||||
dir1 foo bar
|
dir1 foo bar
|
||||||
dir2
|
dir2
|
||||||
}`, false, []Address{
|
}`, false, []address{
|
||||||
{"localhost", "1234"},
|
{"localhost", "1234"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 3,
|
"dir1": 3,
|
||||||
|
@ -109,7 +109,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{`http://localhost https://localhost
|
{`http://localhost https://localhost
|
||||||
dir1 foo bar`, false, []Address{
|
dir1 foo bar`, false, []address{
|
||||||
{"localhost", "http"},
|
{"localhost", "http"},
|
||||||
{"localhost", "https"},
|
{"localhost", "https"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
|
@ -118,7 +118,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
|
|
||||||
{`http://localhost https://localhost {
|
{`http://localhost https://localhost {
|
||||||
dir1 foo bar
|
dir1 foo bar
|
||||||
}`, false, []Address{
|
}`, false, []address{
|
||||||
{"localhost", "http"},
|
{"localhost", "http"},
|
||||||
{"localhost", "https"},
|
{"localhost", "https"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
|
@ -127,7 +127,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
|
|
||||||
{`http://localhost, https://localhost {
|
{`http://localhost, https://localhost {
|
||||||
dir1 foo bar
|
dir1 foo bar
|
||||||
}`, false, []Address{
|
}`, false, []address{
|
||||||
{"localhost", "http"},
|
{"localhost", "http"},
|
||||||
{"localhost", "https"},
|
{"localhost", "https"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
|
@ -135,13 +135,13 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{`http://localhost, {
|
{`http://localhost, {
|
||||||
}`, true, []Address{
|
}`, true, []address{
|
||||||
{"localhost", "http"},
|
{"localhost", "http"},
|
||||||
}, map[string]int{}},
|
}, map[string]int{}},
|
||||||
|
|
||||||
{`host1:80, http://host2.com
|
{`host1:80, http://host2.com
|
||||||
dir1 foo bar
|
dir1 foo bar
|
||||||
dir2 baz`, false, []Address{
|
dir2 baz`, false, []address{
|
||||||
{"host1", "80"},
|
{"host1", "80"},
|
||||||
{"host2.com", "http"},
|
{"host2.com", "http"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
|
@ -151,7 +151,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
|
|
||||||
{`http://host1.com,
|
{`http://host1.com,
|
||||||
http://host2.com,
|
http://host2.com,
|
||||||
https://host3.com`, false, []Address{
|
https://host3.com`, false, []address{
|
||||||
{"host1.com", "http"},
|
{"host1.com", "http"},
|
||||||
{"host2.com", "http"},
|
{"host2.com", "http"},
|
||||||
{"host3.com", "https"},
|
{"host3.com", "https"},
|
||||||
|
@ -161,7 +161,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
dir1 foo {
|
dir1 foo {
|
||||||
bar baz
|
bar baz
|
||||||
}
|
}
|
||||||
dir2`, false, []Address{
|
dir2`, false, []address{
|
||||||
{"host1.com", "1234"},
|
{"host1.com", "1234"},
|
||||||
{"host2.com", "https"},
|
{"host2.com", "https"},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
|
@ -175,7 +175,7 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
}
|
}
|
||||||
dir2 {
|
dir2 {
|
||||||
foo bar
|
foo bar
|
||||||
}`, false, []Address{
|
}`, false, []address{
|
||||||
{"127.0.0.1", ""},
|
{"127.0.0.1", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 5,
|
"dir1": 5,
|
||||||
|
@ -183,13 +183,13 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{`127.0.0.1
|
{`127.0.0.1
|
||||||
unknown_directive`, true, []Address{
|
unknown_directive`, true, []address{
|
||||||
{"127.0.0.1", ""},
|
{"127.0.0.1", ""},
|
||||||
}, map[string]int{}},
|
}, map[string]int{}},
|
||||||
|
|
||||||
{`localhost
|
{`localhost
|
||||||
dir1 {
|
dir1 {
|
||||||
foo`, true, []Address{
|
foo`, true, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 3,
|
"dir1": 3,
|
||||||
|
@ -197,7 +197,15 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
|
|
||||||
{`localhost
|
{`localhost
|
||||||
dir1 {
|
dir1 {
|
||||||
}`, false, []Address{
|
}`, false, []address{
|
||||||
|
{"localhost", ""},
|
||||||
|
}, map[string]int{
|
||||||
|
"dir1": 3,
|
||||||
|
}},
|
||||||
|
|
||||||
|
{`localhost
|
||||||
|
dir1 {
|
||||||
|
} }`, true, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 3,
|
"dir1": 3,
|
||||||
|
@ -209,18 +217,18 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
foo
|
foo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dir2 foo bar`, false, []Address{
|
dir2 foo bar`, false, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 7,
|
"dir1": 7,
|
||||||
"dir2": 3,
|
"dir2": 3,
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{``, false, []Address{}, map[string]int{}},
|
{``, false, []address{}, map[string]int{}},
|
||||||
|
|
||||||
{`localhost
|
{`localhost
|
||||||
dir1 arg1
|
dir1 arg1
|
||||||
import import_test1.txt`, false, []Address{
|
import import_test1.txt`, false, []address{
|
||||||
{"localhost", ""},
|
{"localhost", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 2,
|
"dir1": 2,
|
||||||
|
@ -228,16 +236,20 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
"dir3": 1,
|
"dir3": 1,
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{`import import_test2.txt`, false, []Address{
|
{`import import_test2.txt`, false, []address{
|
||||||
{"host1", ""},
|
{"host1", ""},
|
||||||
}, map[string]int{
|
}, map[string]int{
|
||||||
"dir1": 1,
|
"dir1": 1,
|
||||||
"dir2": 2,
|
"dir2": 2,
|
||||||
}},
|
}},
|
||||||
|
|
||||||
{``, false, []Address{}, map[string]int{}},
|
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
|
||||||
|
|
||||||
{`""`, false, []Address{}, map[string]int{}},
|
{`import not_found.txt`, true, []address{}, map[string]int{}},
|
||||||
|
|
||||||
|
{`""`, false, []address{}, map[string]int{}},
|
||||||
|
|
||||||
|
{``, false, []address{}, map[string]int{}},
|
||||||
} {
|
} {
|
||||||
result, err := testParseOne(test.input)
|
result, err := testParseOne(test.input)
|
||||||
|
|
||||||
|
@ -282,43 +294,43 @@ func TestParseOneAndImport(t *testing.T) {
|
||||||
func TestParseAll(t *testing.T) {
|
func TestParseAll(t *testing.T) {
|
||||||
setupParseTests()
|
setupParseTests()
|
||||||
|
|
||||||
testParseAll := func(input string) ([]ServerBlock, error) {
|
|
||||||
p := testParser(input)
|
|
||||||
return p.parseAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
input string
|
input string
|
||||||
shouldErr bool
|
shouldErr bool
|
||||||
numBlocks int
|
addresses [][]address // addresses per server block, in order
|
||||||
}{
|
}{
|
||||||
{`localhost`, false, 1},
|
{`localhost`, false, [][]address{
|
||||||
|
{{"localhost", ""}},
|
||||||
|
}},
|
||||||
|
|
||||||
{`localhost {
|
{`localhost:1234`, false, [][]address{
|
||||||
dir1
|
[]address{{"localhost", "1234"}},
|
||||||
}`, false, 1},
|
}},
|
||||||
|
|
||||||
{`http://localhost https://localhost
|
{`localhost:1234 {
|
||||||
dir1 foo bar`, false, 1},
|
|
||||||
|
|
||||||
{`http://localhost, https://localhost {
|
|
||||||
dir1 foo bar
|
|
||||||
}`, false, 1},
|
|
||||||
|
|
||||||
{`http://host1.com,
|
|
||||||
http://host2.com,
|
|
||||||
https://host3.com`, false, 1},
|
|
||||||
|
|
||||||
{`host1 {
|
|
||||||
}
|
}
|
||||||
host2 {
|
localhost:2015 {
|
||||||
}`, false, 2},
|
}`, false, [][]address{
|
||||||
|
[]address{{"localhost", "1234"}},
|
||||||
|
[]address{{"localhost", "2015"}},
|
||||||
|
}},
|
||||||
|
|
||||||
{`""`, false, 0},
|
{`localhost:1234, http://host2`, false, [][]address{
|
||||||
|
[]address{{"localhost", "1234"}, {"host2", "http"}},
|
||||||
|
}},
|
||||||
|
|
||||||
{``, false, 0},
|
{`localhost:1234, http://host2,`, true, [][]address{}},
|
||||||
|
|
||||||
|
{`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 {
|
if test.shouldErr && err == nil {
|
||||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
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)
|
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",
|
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||||
i, test.numBlocks, len(results))
|
i, len(test.addresses), len(blocks))
|
||||||
continue
|
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() {
|
func setupParseTests() {
|
||||||
// Set up some bogus directives for testing
|
// Set up some bogus directives for testing
|
||||||
ValidDirectives = map[string]struct{}{
|
ValidDirectives = map[string]struct{}{
|
||||||
"dir1": struct{}{},
|
"dir1": {},
|
||||||
"dir2": struct{}{},
|
"dir2": {},
|
||||||
"dir3": struct{}{},
|
"dir3": {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ func TestBasicAuthParse(t *testing.T) {
|
||||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||||
|
|
||||||
var skipHtpassword bool
|
var skipHtpassword bool
|
||||||
htfh, err := ioutil.TempFile("", "basicauth-")
|
htfh, err := ioutil.TempFile(".", "basicauth-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
||||||
skipHtpassword = true
|
skipHtpassword = true
|
||||||
|
|
|
@ -193,6 +193,10 @@ th a {
|
||||||
margin-top: 70px;
|
margin-top: 70px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -240,7 +244,7 @@ th a {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{{if .IsDir}}📂{{else}}📄{{end}}
|
{{if .IsDir}}📂{{else}}📄{{end}}
|
||||||
<a href="{{.URL}}">{{.Name}}</a>
|
<a href="{{.URL}}" class="name">{{.Name}}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{.HumanSize}}</td>
|
<td>{{.HumanSize}}</td>
|
||||||
<td class="hideable">{{.HumanModTime "01/02/2006 3:04:05 PM -0700"}}</td>
|
<td class="hideable">{{.HumanModTime "01/02/2006 3:04:05 PM -0700"}}</td>
|
||||||
|
|
|
@ -1,11 +1,68 @@
|
||||||
package setup
|
package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mholt/caddy/config/parse"
|
"github.com/mholt/caddy/config/parse"
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
"github.com/mholt/caddy/server"
|
"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 {
|
type Controller struct {
|
||||||
*server.Config
|
*server.Config
|
||||||
parse.Dispenser
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
package setup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mholt/caddy/config/parse"
|
|
||||||
"github.com/mholt/caddy/middleware"
|
|
||||||
"github.com/mholt/caddy/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewTestController creates a new *Controller for
|
|
||||||
// the input specified, with a filename of "Testfile"
|
|
||||||
func NewTestController(input string) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
Config: &server.Config{},
|
|
||||||
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmptyNext is a no-op function that can be passed into
|
|
||||||
// middleware.Middleware functions so that the assignment
|
|
||||||
// to the Next field of the Handler can be tested.
|
|
||||||
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
return 0, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// SameNext does a pointer comparison between next1 and next2.
|
|
||||||
func SameNext(next1, next2 middleware.Handler) bool {
|
|
||||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
|
||||||
}
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/hashicorp/go-syslog"
|
"github.com/hashicorp/go-syslog"
|
||||||
|
@ -105,7 +105,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Error page; ensure it exists
|
// Error page; ensure it exists
|
||||||
where = path.Join(c.Root, where)
|
where = filepath.Join(c.Root, where)
|
||||||
f, err := os.Open(where)
|
f, err := os.Open(where)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error())
|
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,
|
SoftwareName: c.AppName,
|
||||||
SoftwareVersion: c.AppVersion,
|
SoftwareVersion: c.AppVersion,
|
||||||
ServerName: c.Host,
|
ServerName: c.Host,
|
||||||
ServerPort: c.Port, // BUG: This is not known until the server blocks are split up...
|
ServerPort: c.Port,
|
||||||
}
|
}
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,62 +68,8 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
||||||
|
|
||||||
// Load any other configuration parameters
|
// Load any other configuration parameters
|
||||||
for c.NextBlock() {
|
for c.NextBlock() {
|
||||||
switch c.Val() {
|
if err := loadParams(c, md); err != nil {
|
||||||
case "ext":
|
return mdconfigs, err
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,3 +83,70 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
||||||
|
|
||||||
return mdconfigs, nil
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package setup
|
package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -92,7 +93,7 @@ func TestMarkdownStaticGen(t *testing.T) {
|
||||||
t.Fatalf("An error occured when getting the file content: %v", err)
|
t.Fatalf("An error occured when getting the file content: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedBody := `<!DOCTYPE html>
|
expectedBody := []byte(`<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>first_post</title>
|
<title>first_post</title>
|
||||||
|
@ -104,9 +105,10 @@ func TestMarkdownStaticGen(t *testing.T) {
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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)
|
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
|
||||||
|
|
62
config/setup/mime.go
Normal file
62
config/setup/mime.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
|
"github.com/mholt/caddy/middleware/mime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mime configures a new mime middleware instance.
|
||||||
|
func Mime(c *Controller) (middleware.Middleware, error) {
|
||||||
|
configs, err := mimeParse(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(next middleware.Handler) middleware.Handler {
|
||||||
|
return mime.Mime{Next: next, Configs: configs}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mimeParse(c *Controller) ([]mime.Config, error) {
|
||||||
|
var configs []mime.Config
|
||||||
|
|
||||||
|
for c.Next() {
|
||||||
|
// At least one extension is required
|
||||||
|
|
||||||
|
args := c.RemainingArgs()
|
||||||
|
switch len(args) {
|
||||||
|
case 2:
|
||||||
|
if err := validateExt(args[0]); err != nil {
|
||||||
|
return configs, err
|
||||||
|
}
|
||||||
|
configs = append(configs, mime.Config{Ext: args[0], ContentType: args[1]})
|
||||||
|
case 1:
|
||||||
|
return configs, c.ArgErr()
|
||||||
|
case 0:
|
||||||
|
for c.NextBlock() {
|
||||||
|
ext := c.Val()
|
||||||
|
if err := validateExt(ext); err != nil {
|
||||||
|
return configs, err
|
||||||
|
}
|
||||||
|
if !c.NextArg() {
|
||||||
|
return configs, c.ArgErr()
|
||||||
|
}
|
||||||
|
configs = append(configs, mime.Config{Ext: ext, ContentType: c.Val()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateExt checks for valid file name extension.
|
||||||
|
func validateExt(ext string) error {
|
||||||
|
if !strings.HasPrefix(ext, ".") {
|
||||||
|
return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
59
config/setup/mime_test.go
Normal file
59
config/setup/mime_test.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware/mime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMime(t *testing.T) {
|
||||||
|
|
||||||
|
c := NewTestController(`mime .txt text/plain`)
|
||||||
|
|
||||||
|
mid, err := Mime(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no errors, but got: %v", err)
|
||||||
|
}
|
||||||
|
if mid == nil {
|
||||||
|
t.Fatal("Expected middleware, was nil instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := mid(EmptyNext)
|
||||||
|
myHandler, ok := handler.(mime.Mime)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected handler to be type Mime, got: %#v", handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !SameNext(myHandler.Next, EmptyNext) {
|
||||||
|
t.Error("'Next' field of handler was not set properly")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
shouldErr bool
|
||||||
|
}{
|
||||||
|
{`mime {`, true},
|
||||||
|
{`mime {}`, true},
|
||||||
|
{`mime a b`, true},
|
||||||
|
{`mime a {`, true},
|
||||||
|
{`mime { txt f } `, true},
|
||||||
|
{`mime { html } `, true},
|
||||||
|
{`mime {
|
||||||
|
.html text/html
|
||||||
|
.txt text/plain
|
||||||
|
} `, false},
|
||||||
|
{`mime { .html text/html } `, false},
|
||||||
|
{`mime { .html
|
||||||
|
} `, true},
|
||||||
|
{`mime .txt text/plain`, false},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
c := NewTestController(test.input)
|
||||||
|
m, err := mimeParse(c)
|
||||||
|
if test.shouldErr && err == nil {
|
||||||
|
t.Errorf("Test %v: Expected error but found nil %v", i, m)
|
||||||
|
} else if !test.shouldErr && err != nil {
|
||||||
|
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,11 @@ import (
|
||||||
|
|
||||||
// Proxy configures a new Proxy middleware instance.
|
// Proxy configures a new Proxy middleware instance.
|
||||||
func Proxy(c *Controller) (middleware.Middleware, error) {
|
func Proxy(c *Controller) (middleware.Middleware, error) {
|
||||||
if upstreams, err := proxy.NewStaticUpstreams(c.Dispenser); err == nil {
|
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return func(next middleware.Handler) middleware.Handler {
|
return func(next middleware.Handler) middleware.Handler {
|
||||||
return proxy.Proxy{Next: next, Upstreams: upstreams}
|
return proxy.Proxy{Next: next, Upstreams: upstreams}
|
||||||
}, nil
|
}, nil
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,13 +37,13 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||||
// checkAndSaveRule checks the rule for validity (except the redir code)
|
// checkAndSaveRule checks the rule for validity (except the redir code)
|
||||||
// and saves it if it's valid, or returns an error.
|
// and saves it if it's valid, or returns an error.
|
||||||
checkAndSaveRule := func(rule redirect.Rule) 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")
|
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, otherRule := range redirects {
|
for _, otherRule := range redirects {
|
||||||
if otherRule.From == rule.From {
|
if otherRule.FromPath == rule.FromPath {
|
||||||
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.From, otherRule.To)
|
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
|
var rule redirect.Rule
|
||||||
|
|
||||||
|
if c.Config.TLS.Enabled {
|
||||||
|
rule.FromScheme = "https"
|
||||||
|
} else {
|
||||||
|
rule.FromScheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
// Set initial redirect code
|
// Set initial redirect code
|
||||||
// BUG: If the code is specified for a whole block and that code is invalid,
|
// 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
|
// 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)
|
// To specified (catch-all redirect)
|
||||||
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
|
// 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.
|
// As such, this feature remains undocumented.
|
||||||
rule.From = "/"
|
rule.FromPath = "/"
|
||||||
rule.To = insideArgs[0]
|
rule.To = insideArgs[0]
|
||||||
case 2:
|
case 2:
|
||||||
// From and To specified
|
// From and To specified
|
||||||
rule.From = insideArgs[0]
|
rule.FromPath = insideArgs[0]
|
||||||
rule.To = insideArgs[1]
|
rule.To = insideArgs[1]
|
||||||
case 3:
|
case 3:
|
||||||
// From, To, and Code specified
|
// From, To, and Code specified
|
||||||
rule.From = insideArgs[0]
|
rule.FromPath = insideArgs[0]
|
||||||
rule.To = insideArgs[1]
|
rule.To = insideArgs[1]
|
||||||
err := setRedirCode(insideArgs[2], &rule)
|
err := setRedirCode(insideArgs[2], &rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -110,16 +116,23 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||||
|
|
||||||
if !hadOptionalBlock {
|
if !hadOptionalBlock {
|
||||||
var rule redirect.Rule
|
var rule redirect.Rule
|
||||||
|
|
||||||
|
if c.Config.TLS.Enabled {
|
||||||
|
rule.FromScheme = "https"
|
||||||
|
} else {
|
||||||
|
rule.FromScheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
rule.Code = http.StatusMovedPermanently // default
|
rule.Code = http.StatusMovedPermanently // default
|
||||||
|
|
||||||
switch len(args) {
|
switch len(args) {
|
||||||
case 1:
|
case 1:
|
||||||
// To specified (catch-all redirect)
|
// To specified (catch-all redirect)
|
||||||
rule.From = "/"
|
rule.FromPath = "/"
|
||||||
rule.To = args[0]
|
rule.To = args[0]
|
||||||
case 2:
|
case 2:
|
||||||
// To and Code specified (catch-all redirect)
|
// To and Code specified (catch-all redirect)
|
||||||
rule.From = "/"
|
rule.FromPath = "/"
|
||||||
rule.To = args[0]
|
rule.To = args[0]
|
||||||
err := setRedirCode(args[1], &rule)
|
err := setRedirCode(args[1], &rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -127,7 +140,7 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
// From, To, and Code specified
|
// From, To, and Code specified
|
||||||
rule.From = args[0]
|
rule.FromPath = args[0]
|
||||||
rule.To = args[1]
|
rule.To = args[1]
|
||||||
err := setRedirCode(args[2], &rule)
|
err := setRedirCode(args[2], &rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -149,12 +162,12 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||||
|
|
||||||
// httpRedirs is a list of supported HTTP redirect codes.
|
// httpRedirs is a list of supported HTTP redirect codes.
|
||||||
var httpRedirs = map[string]int{
|
var httpRedirs = map[string]int{
|
||||||
"300": 300, // Multiple Choices
|
"300": http.StatusMultipleChoices,
|
||||||
"301": 301, // Moved Permanently
|
"301": http.StatusMovedPermanently,
|
||||||
"302": 302, // Found (NOT CORRECT for "Temporary Redirect", see 307)
|
"302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307)
|
||||||
"303": 303, // See Other
|
"303": http.StatusSeeOther,
|
||||||
"304": 304, // Not Modified
|
"304": http.StatusNotModified,
|
||||||
"305": 305, // Use Proxy
|
"305": http.StatusUseProxy,
|
||||||
"307": 307, // Temporary Redirect
|
"307": http.StatusTemporaryRedirect,
|
||||||
"308": 308, // Permanent Redirect
|
"308": 308, // Permanent Redirect
|
||||||
}
|
}
|
||||||
|
|
108
config/setup/root_test.go
Normal file
108
config/setup/root_test.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoot(t *testing.T) {
|
||||||
|
|
||||||
|
// Predefined error substrings
|
||||||
|
parseErrContent := "Parse error:"
|
||||||
|
unableToAccessErrContent := "Unable to access root path"
|
||||||
|
|
||||||
|
existingDirPath, err := getTempDirPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir")
|
||||||
|
|
||||||
|
existingFile, err := ioutil.TempFile("", "root_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
existingFile.Close()
|
||||||
|
os.Remove(existingFile.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
|
inaccessiblePath := getInaccessiblePath(existingFile.Name())
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
shouldErr bool
|
||||||
|
expectedRoot string // expected root, set to the controller. Empty for negative cases.
|
||||||
|
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||||
|
}{
|
||||||
|
// positive
|
||||||
|
{
|
||||||
|
fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "",
|
||||||
|
},
|
||||||
|
// negative
|
||||||
|
{
|
||||||
|
`root `, true, "", parseErrContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fmt.Sprintf(`root {
|
||||||
|
%s
|
||||||
|
}`, existingDirPath), true, "", parseErrContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
c := NewTestController(test.input)
|
||||||
|
mid, err := Root(c)
|
||||||
|
|
||||||
|
if test.shouldErr && err == nil {
|
||||||
|
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !test.shouldErr {
|
||||||
|
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||||
|
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the Root method always returns a nil middleware
|
||||||
|
if mid != nil {
|
||||||
|
t.Errorf("Middware, returned from Root() was not nil: %v", mid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check c.Root only if we are in a positive test.
|
||||||
|
if !test.shouldErr && test.expectedRoot != c.Root {
|
||||||
|
t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned.
|
||||||
|
func getTempDirPath() (string, error) {
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
|
||||||
|
_, err := os.Stat(tempDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInaccessiblePath(file string) string {
|
||||||
|
// null byte in filename is not allowed on Windows AND unix
|
||||||
|
return filepath.Join("C:", "file\x00name")
|
||||||
|
}
|
|
@ -20,6 +20,8 @@ func Shutdown(c *Controller) (middleware.Middleware, error) {
|
||||||
// using c to parse the line. It appends the callback function
|
// using c to parse the line. It appends the callback function
|
||||||
// to the list of callback functions passed in by reference.
|
// to the list of callback functions passed in by reference.
|
||||||
func registerCallback(c *Controller, list *[]func() error) error {
|
func registerCallback(c *Controller, list *[]func() error) error {
|
||||||
|
var funcs []func() error
|
||||||
|
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
args := c.RemainingArgs()
|
args := c.RemainingArgs()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
|
@ -46,13 +48,15 @@ func registerCallback(c *Controller, list *[]func() error) error {
|
||||||
|
|
||||||
if nonblock {
|
if nonblock {
|
||||||
return cmd.Start()
|
return cmd.Start()
|
||||||
} else {
|
}
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
funcs = append(funcs, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
*list = append(*list, fn)
|
return c.OncePerServerBlock(func() error {
|
||||||
}
|
*list = append(*list, funcs...)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,26 @@ package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mholt/caddy/middleware"
|
"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) {
|
func WebSocket(c *Controller) (middleware.Middleware, error) {
|
||||||
|
|
||||||
websocks, err := webSocketParse(c)
|
websocks, err := webSocketParse(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
websockets.GatewayInterface = c.AppName + "-CGI/1.1"
|
websocket.GatewayInterface = c.AppName + "-CGI/1.1"
|
||||||
websockets.ServerSoftware = c.AppName + "/" + c.AppVersion
|
websocket.ServerSoftware = c.AppName + "/" + c.AppVersion
|
||||||
|
|
||||||
return func(next middleware.Handler) middleware.Handler {
|
return func(next middleware.Handler) middleware.Handler {
|
||||||
return websockets.WebSockets{Next: next, Sockets: websocks}
|
return websocket.WebSocket{Next: next, Sockets: websocks}
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func webSocketParse(c *Controller) ([]websockets.Config, error) {
|
func webSocketParse(c *Controller) ([]websocket.Config, error) {
|
||||||
var websocks []websockets.Config
|
var websocks []websocket.Config
|
||||||
var respawn bool
|
var respawn bool
|
||||||
|
|
||||||
optionalBlock := func() (hadBlock bool, err error) {
|
optionalBlock := func() (hadBlock bool, err error) {
|
||||||
|
@ -74,7 +74,7 @@ func webSocketParse(c *Controller) ([]websockets.Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
websocks = append(websocks, websockets.Config{
|
websocks = append(websocks, websocket.Config{
|
||||||
Path: path,
|
Path: path,
|
||||||
Command: cmd,
|
Command: cmd,
|
||||||
Arguments: args,
|
Arguments: args,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package setup
|
package setup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mholt/caddy/middleware/websockets"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWebSocket(t *testing.T) {
|
func TestWebSocket(t *testing.T) {
|
||||||
|
@ -20,10 +21,10 @@ func TestWebSocket(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := mid(EmptyNext)
|
handler := mid(EmptyNext)
|
||||||
myHandler, ok := handler.(websockets.WebSockets)
|
myHandler, ok := handler.(websocket.WebSocket)
|
||||||
|
|
||||||
if !ok {
|
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 != "/" {
|
if myHandler.Sockets[0].Path != "/" {
|
||||||
|
@ -38,15 +39,15 @@ func TestWebSocketParse(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
inputWebSocketConfig string
|
inputWebSocketConfig string
|
||||||
shouldErr bool
|
shouldErr bool
|
||||||
expectedWebSocketConfig []websockets.Config
|
expectedWebSocketConfig []websocket.Config
|
||||||
}{
|
}{
|
||||||
{`websocket /api1 cat`, false, []websockets.Config{{
|
{`websocket /api1 cat`, false, []websocket.Config{{
|
||||||
Path: "/api1",
|
Path: "/api1",
|
||||||
Command: "cat",
|
Command: "cat",
|
||||||
}}},
|
}}},
|
||||||
|
|
||||||
{`websocket /api3 cat
|
{`websocket /api3 cat
|
||||||
websocket /api4 cat `, false, []websockets.Config{{
|
websocket /api4 cat `, false, []websocket.Config{{
|
||||||
Path: "/api3",
|
Path: "/api3",
|
||||||
Command: "cat",
|
Command: "cat",
|
||||||
}, {
|
}, {
|
||||||
|
|
8
dist/CHANGES.txt
vendored
8
dist/CHANGES.txt
vendored
|
@ -1,12 +1,18 @@
|
||||||
CHANGES
|
CHANGES
|
||||||
|
|
||||||
<master>
|
<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
|
- 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
|
- core: Caddyfile as command line argument
|
||||||
- errors: Can write full stack trace to HTTP response for debugging
|
- errors: Can write full stack trace to HTTP response for debugging
|
||||||
- errors, log: Roll log files after certain size or age
|
- errors, log: Roll log files after certain size or age
|
||||||
- proxy: Fix for 32-bit architectures
|
- proxy: Fix for 32-bit architectures
|
||||||
|
- rewrite: Better compatibility with fastcgi and PHP apps
|
||||||
- templates: Added .StripExt and .StripHTML methods
|
- templates: Added .StripExt and .StripHTML methods
|
||||||
- Internal improvements and minor bug fixes
|
- Internal improvements and minor bug fixes
|
||||||
|
|
||||||
|
|
2
dist/README.txt
vendored
2
dist/README.txt
vendored
|
@ -1,4 +1,4 @@
|
||||||
CADDY 0.7.5
|
CADDY 0.7.6
|
||||||
|
|
||||||
Website
|
Website
|
||||||
https://caddyserver.com
|
https://caddyserver.com
|
||||||
|
|
11
main.go
11
main.go
|
@ -26,7 +26,7 @@ var (
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")")
|
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.BoolVar(&app.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||||
flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site")
|
flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site")
|
||||||
|
@ -51,7 +51,7 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load address configurations from highest priority input
|
// Load config from file
|
||||||
addresses, err := loadConfigs()
|
addresses, err := loadConfigs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -63,7 +63,7 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
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)
|
app.Wg.Add(1)
|
||||||
go func(s *server.Server) {
|
go func(s *server.Server) {
|
||||||
defer app.Wg.Done()
|
defer app.Wg.Done()
|
||||||
|
@ -125,10 +125,9 @@ func isLocalhost(s string) bool {
|
||||||
|
|
||||||
// loadConfigs loads configuration from a file or stdin (piped).
|
// loadConfigs loads configuration from a file or stdin (piped).
|
||||||
// The configurations are grouped by bind address.
|
// 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.
|
// in this order: 1. -conf flag, 2. stdin, 3. command line argument 4. Caddyfile.
|
||||||
// If none of those are available, a default configuration is
|
// If none of those are available, a default configuration is loaded.
|
||||||
// loaded.
|
|
||||||
func loadConfigs() (config.Group, error) {
|
func loadConfigs() (config.Group, error) {
|
||||||
// -conf flag
|
// -conf flag
|
||||||
if conf != "" {
|
if conf != "" {
|
||||||
|
|
|
@ -78,6 +78,7 @@ type Rule struct {
|
||||||
Resources []string
|
Resources []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PasswordMatcher determines whether a password mathes a rule.
|
||||||
type PasswordMatcher func(pw string) bool
|
type PasswordMatcher func(pw string) bool
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -137,6 +138,8 @@ func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
|
||||||
return scanner.Err()
|
return scanner.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlainMatcher returns a PasswordMatcher that does a constant-time
|
||||||
|
// byte-wise comparison.
|
||||||
func PlainMatcher(passw string) PasswordMatcher {
|
func PlainMatcher(passw string) PasswordMatcher {
|
||||||
return func(pw string) bool {
|
return func(pw string) bool {
|
||||||
return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1
|
return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mholt/caddy/middleware"
|
"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")
|
t.Skipf("Error creating temp file (%v), will skip htpassword test")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer os.Remove(htfh.Name())
|
||||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||||
}
|
}
|
||||||
htfh.Close()
|
htfh.Close()
|
||||||
defer os.Remove(htfh.Name())
|
|
||||||
|
|
||||||
for i, username := range []string{"sha1", "md5"} {
|
for i, username := range []string{"sha1", "md5"} {
|
||||||
rule := Rule{Username: username, Resources: []string{"/testing"}}
|
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.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
|
||||||
}
|
}
|
||||||
t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password)
|
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")
|
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 the query 'sort' or 'order' is empty, check the cookies
|
||||||
if listing.Sort == "" || listing.Order == "" {
|
if listing.Sort == "" {
|
||||||
sortCookie, sortErr := r.Cookie("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 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.Sort = "name"
|
||||||
listing.Order = "asc"
|
|
||||||
} else { // if we have values in the cookies, use them
|
} else { // if we have values in the cookies, use them
|
||||||
listing.Sort = sortCookie.Value
|
listing.Sort = sortCookie.Value
|
||||||
listing.Order = orderCookie.Value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else { // save the query value of 'sort' and 'order' as cookies
|
} 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: "sort", Value: listing.Sort, Path: "/"})
|
||||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, 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
|
// Apply the sorting
|
||||||
listing.applySort()
|
listing.applySort()
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,17 @@ package browse
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/mholt/caddy/middleware"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// "sort" package has "IsSorted" function, but no "IsReversed";
|
// "sort" package has "IsSorted" function, but no "IsReversed";
|
||||||
|
@ -115,7 +117,7 @@ func TestBrowseTemplate(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
Root: "./testdata",
|
Root: "./testdata",
|
||||||
Configs: []Config{
|
Configs: []Config{
|
||||||
Config{
|
{
|
||||||
PathScope: "/photos",
|
PathScope: "/photos",
|
||||||
Template: tmpl,
|
Template: tmpl,
|
||||||
},
|
},
|
||||||
|
@ -129,9 +131,9 @@ func TestBrowseTemplate(t *testing.T) {
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
b.ServeHTTP(rec, req)
|
code, err := b.ServeHTTP(rec, req)
|
||||||
if rec.Code != http.StatusOK {
|
if code != http.StatusOK {
|
||||||
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, rec.Code)
|
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
respBody := rec.Body.String()
|
respBody := rec.Body.String()
|
||||||
|
@ -149,6 +151,8 @@ func TestBrowseTemplate(t *testing.T) {
|
||||||
|
|
||||||
<a href="test2.html">test2.html</a><br>
|
<a href="test2.html">test2.html</a><br>
|
||||||
|
|
||||||
|
<a href="test3.html">test3.html</a><br>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`
|
`
|
||||||
|
@ -168,37 +172,19 @@ func TestBrowseJson(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
Root: "./testdata",
|
Root: "./testdata",
|
||||||
Configs: []Config{
|
Configs: []Config{
|
||||||
Config{
|
{
|
||||||
PathScope: "/photos",
|
PathScope: "/photos/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "/photos/", nil)
|
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
|
||||||
if err != nil {
|
testDataPath := b.Root + "/photos/"
|
||||||
t.Fatalf("Test: Could not create HTTP request: %v", err)
|
file, err := os.Open(testDataPath)
|
||||||
}
|
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsPermission(err) {
|
if os.IsPermission(err) {
|
||||||
t.Fatalf("Os Permission Error")
|
t.Fatalf("Os Permission Error")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
@ -207,9 +193,18 @@ func TestBrowseJson(t *testing.T) {
|
||||||
t.Fatalf("Unable to Read Contents of the directory")
|
t.Fatalf("Unable to Read Contents of the directory")
|
||||||
}
|
}
|
||||||
var fileinfos []FileInfo
|
var fileinfos []FileInfo
|
||||||
for _, f := range files {
|
|
||||||
|
for i, f := range files {
|
||||||
name := f.Name()
|
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() {
|
if f.IsDir() {
|
||||||
name += "/"
|
name += "/"
|
||||||
}
|
}
|
||||||
|
@ -221,23 +216,103 @@ func TestBrowseJson(t *testing.T) {
|
||||||
Name: f.Name(),
|
Name: f.Name(),
|
||||||
Size: f.Size(),
|
Size: f.Size(),
|
||||||
URL: url.String(),
|
URL: url.String(),
|
||||||
ModTime: f.ModTime(),
|
ModTime: chTime,
|
||||||
Mode: f.Mode(),
|
Mode: f.Mode(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
listing := Listing{
|
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
|
||||||
Items: fileinfos,
|
|
||||||
}
|
tests := []struct {
|
||||||
listing.Sort = "name"
|
QueryUrl string
|
||||||
listing.Order = "asc"
|
SortBy string
|
||||||
listing.applySort()
|
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},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
marsh, err := json.Marshal(listing.Items)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to Marshal the listing ")
|
t.Fatalf("Unable to Marshal the listing ")
|
||||||
}
|
}
|
||||||
expectedJsonString := string(marsh)
|
expectedJSON := string(marsh)
|
||||||
if actualJsonResponseString != expectedJsonString {
|
|
||||||
t.Errorf("Json response string doesnt match the expected Json response ")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
middleware/browse/testdata/photos/test3.html
vendored
Normal file
3
middleware/browse/testdata/photos/test3.html
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
</html>
|
138
middleware/commands_test.go
Normal file
138
middleware/commands_test.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitCommandAndArgs(t *testing.T) {
|
||||||
|
var parseErrorContent = "error parsing command:"
|
||||||
|
var noCommandErrContent = "no command contained in"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expectedCommand string
|
||||||
|
expectedArgs []string
|
||||||
|
expectedErrContent string
|
||||||
|
}{
|
||||||
|
// Test case 0 - emtpy command
|
||||||
|
{
|
||||||
|
input: ``,
|
||||||
|
expectedCommand: ``,
|
||||||
|
expectedArgs: nil,
|
||||||
|
expectedErrContent: noCommandErrContent,
|
||||||
|
},
|
||||||
|
// Test case 1 - command without arguments
|
||||||
|
{
|
||||||
|
input: `command`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: nil,
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 2 - command with single argument
|
||||||
|
{
|
||||||
|
input: `command arg1`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1`},
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 3 - command with multiple arguments
|
||||||
|
{
|
||||||
|
input: `command arg1 arg2`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1`, `arg2`},
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 4 - command with single argument with space character - in quotes
|
||||||
|
{
|
||||||
|
input: `command "arg1 arg1"`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1 arg1`},
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 4 - command with single argument with space character - escaped
|
||||||
|
{
|
||||||
|
input: `command arg1\ arg1`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1 arg1`},
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 6 - command with escaped quote character
|
||||||
|
{
|
||||||
|
input: `command "arg1 \" arg1"`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1 " arg1`},
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 7 - command with escaped backslash
|
||||||
|
{
|
||||||
|
input: `command '\arg1'`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`\arg1`},
|
||||||
|
expectedErrContent: ``,
|
||||||
|
},
|
||||||
|
// Test case 8 - command with comments
|
||||||
|
{
|
||||||
|
input: `command arg1 #comment1 comment2`,
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1`},
|
||||||
|
expectedErrContent: "",
|
||||||
|
},
|
||||||
|
// Test case 9 - command with multiple spaces and tab character
|
||||||
|
{
|
||||||
|
input: "command arg1 arg2\targ3",
|
||||||
|
expectedCommand: `command`,
|
||||||
|
expectedArgs: []string{`arg1`, `arg2`, "arg3"},
|
||||||
|
expectedErrContent: "",
|
||||||
|
},
|
||||||
|
// Test case 10 - command with unclosed quotes
|
||||||
|
{
|
||||||
|
input: `command "arg1 arg2`,
|
||||||
|
expectedCommand: "",
|
||||||
|
expectedArgs: nil,
|
||||||
|
expectedErrContent: parseErrorContent,
|
||||||
|
},
|
||||||
|
// Test case 11 - command with unclosed quotes
|
||||||
|
{
|
||||||
|
input: `command 'arg1 arg2"`,
|
||||||
|
expectedCommand: "",
|
||||||
|
expectedArgs: nil,
|
||||||
|
expectedErrContent: parseErrorContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
|
||||||
|
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
|
||||||
|
actualCommand, actualArgs, actualErr := SplitCommandAndArgs(test.input)
|
||||||
|
|
||||||
|
// test if error matches expectation
|
||||||
|
if test.expectedErrContent != "" {
|
||||||
|
if actualErr == nil {
|
||||||
|
t.Errorf(errorPrefix+"Expected error with content [%s], found no error."+errorSuffix, test.expectedErrContent)
|
||||||
|
} else if !strings.Contains(actualErr.Error(), test.expectedErrContent) {
|
||||||
|
t.Errorf(errorPrefix+"Expected error with content [%s], found [%v]."+errorSuffix, test.expectedErrContent, actualErr)
|
||||||
|
}
|
||||||
|
} else if actualErr != nil {
|
||||||
|
t.Errorf(errorPrefix+"Expected no error, found [%v]."+errorSuffix, actualErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if command matches
|
||||||
|
if test.expectedCommand != actualCommand {
|
||||||
|
t.Errorf("Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if arguments match
|
||||||
|
if len(test.expectedArgs) != len(actualArgs) {
|
||||||
|
t.Errorf("Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, actualArg := range actualArgs {
|
||||||
|
expectedArg := test.expectedArgs[j]
|
||||||
|
if actualArg != expectedArg {
|
||||||
|
t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,9 +37,8 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
fmt.Fprintln(w, errMsg)
|
fmt.Fprintln(w, errMsg)
|
||||||
return 0, err // returning < 400 signals that a response has been written
|
return 0, err // returning < 400 signals that a response has been written
|
||||||
} else {
|
|
||||||
h.Log.Println(errMsg)
|
|
||||||
}
|
}
|
||||||
|
h.Log.Println(errMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
if status >= 400 {
|
if status >= 400 {
|
||||||
|
|
56
middleware/fastcgi/fastcgi.go
Normal file → Executable file
56
middleware/fastcgi/fastcgi.go
Normal file → Executable file
|
@ -58,17 +58,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to FastCGI gateway
|
// Connect to FastCGI gateway
|
||||||
var fcgi *FCGIClient
|
fcgi, err := getClient(&rule)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusBadGateway, err
|
return http.StatusBadGateway, err
|
||||||
}
|
}
|
||||||
|
@ -102,13 +92,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
return http.StatusBadGateway, err
|
return http.StatusBadGateway, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the response header
|
writeHeader(w, resp)
|
||||||
for key, vals := range resp.Header {
|
|
||||||
for _, val := range vals {
|
|
||||||
w.Header().Add(key, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.WriteHeader(resp.StatusCode)
|
|
||||||
|
|
||||||
// Write the response body
|
// Write the response body
|
||||||
// TODO: If this has an error, the response will already be
|
// 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)
|
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 {
|
func (h Handler) exists(path string) bool {
|
||||||
if _, err := os.Stat(h.Root + path); err == nil {
|
if _, err := os.Stat(h.Root + path); err == nil {
|
||||||
return true
|
return true
|
||||||
|
@ -166,6 +170,20 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
|
||||||
scriptFilename = absPath
|
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
|
// Some variables are unused but cleared explicitly to prevent
|
||||||
// the parent environment from interfering.
|
// the parent environment from interfering.
|
||||||
env = map[string]string{
|
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_ROOT": h.AbsRoot,
|
||||||
"DOCUMENT_URI": docURI,
|
"DOCUMENT_URI": docURI,
|
||||||
"HTTP_HOST": r.Host, // added here, since not always part of headers
|
"HTTP_HOST": r.Host, // added here, since not always part of headers
|
||||||
"REQUEST_URI": r.URL.RequestURI(),
|
"REQUEST_URI": reqURI,
|
||||||
"SCRIPT_FILENAME": scriptFilename,
|
"SCRIPT_FILENAME": scriptFilename,
|
||||||
"SCRIPT_NAME": scriptName,
|
"SCRIPT_NAME": scriptName,
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,45 +30,45 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const FCGI_LISTENSOCK_FILENO uint8 = 0
|
const FCGIListenSockFileno uint8 = 0
|
||||||
const FCGI_HEADER_LEN uint8 = 8
|
const FCGIHeaderLen uint8 = 8
|
||||||
const VERSION_1 uint8 = 1
|
const Version1 uint8 = 1
|
||||||
const FCGI_NULL_REQUEST_ID uint8 = 0
|
const FCGINullRequestID uint8 = 0
|
||||||
const FCGI_KEEP_CONN uint8 = 1
|
const FCGIKeepConn uint8 = 1
|
||||||
const doubleCRLF = "\r\n\r\n"
|
const doubleCRLF = "\r\n\r\n"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FCGI_BEGIN_REQUEST uint8 = iota + 1
|
BeginRequest uint8 = iota + 1
|
||||||
FCGI_ABORT_REQUEST
|
AbortRequest
|
||||||
FCGI_END_REQUEST
|
EndRequest
|
||||||
FCGI_PARAMS
|
Params
|
||||||
FCGI_STDIN
|
Stdin
|
||||||
FCGI_STDOUT
|
Stdout
|
||||||
FCGI_STDERR
|
Stderr
|
||||||
FCGI_DATA
|
Data
|
||||||
FCGI_GET_VALUES
|
GetValues
|
||||||
FCGI_GET_VALUES_RESULT
|
GetValuesResult
|
||||||
FCGI_UNKNOWN_TYPE
|
UnknownType
|
||||||
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
|
MaxType = UnknownType
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FCGI_RESPONDER uint8 = iota + 1
|
Responder uint8 = iota + 1
|
||||||
FCGI_AUTHORIZER
|
Authorizer
|
||||||
FCGI_FILTER
|
Filter
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FCGI_REQUEST_COMPLETE uint8 = iota
|
RequestComplete uint8 = iota
|
||||||
FCGI_CANT_MPX_CONN
|
CantMultiplexConns
|
||||||
FCGI_OVERLOADED
|
Overloaded
|
||||||
FCGI_UNKNOWN_ROLE
|
UnknownRole
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FCGI_MAX_CONNS string = "MAX_CONNS"
|
MaxConns string = "MAX_CONNS"
|
||||||
FCGI_MAX_REQS string = "MAX_REQS"
|
MaxRequests string = "MAX_REQS"
|
||||||
FCGI_MPXS_CONNS string = "MPXS_CONNS"
|
MultiplexConns string = "MPXS_CONNS"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -79,7 +79,7 @@ const (
|
||||||
type header struct {
|
type header struct {
|
||||||
Version uint8
|
Version uint8
|
||||||
Type uint8
|
Type uint8
|
||||||
Id uint16
|
ID uint16
|
||||||
ContentLength uint16
|
ContentLength uint16
|
||||||
PaddingLength uint8
|
PaddingLength uint8
|
||||||
Reserved uint8
|
Reserved uint8
|
||||||
|
@ -92,7 +92,7 @@ var pad [maxPad]byte
|
||||||
func (h *header) init(recType uint8, reqID uint16, contentLength int) {
|
func (h *header) init(recType uint8, reqID uint16, contentLength int) {
|
||||||
h.Version = 1
|
h.Version = 1
|
||||||
h.Type = recType
|
h.Type = recType
|
||||||
h.Id = reqID
|
h.ID = reqID
|
||||||
h.ContentLength = uint16(contentLength)
|
h.ContentLength = uint16(contentLength)
|
||||||
h.PaddingLength = uint8(-contentLength & 7)
|
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")
|
err = errors.New("fcgi: invalid header version")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if rec.h.Type == FCGI_END_REQUEST {
|
if rec.h.Type == EndRequest {
|
||||||
err = io.EOF
|
err = io.EOF
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -126,13 +126,15 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FCGIClient implements a FastCGI client, which is a standard for
|
||||||
|
// interfacing external applications with Web servers.
|
||||||
type FCGIClient struct {
|
type FCGIClient struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
rwc io.ReadWriteCloser
|
rwc io.ReadWriteCloser
|
||||||
h header
|
h header
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
keepAlive bool
|
keepAlive bool
|
||||||
reqId uint16
|
reqID uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dial connects to the fcgi responder at the specified network address.
|
// 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{
|
fcgi = &FCGIClient{
|
||||||
rwc: conn,
|
rwc: conn,
|
||||||
keepAlive: false,
|
keepAlive: false,
|
||||||
reqId: 1,
|
reqID: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -163,7 +165,7 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
c.buf.Reset()
|
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 {
|
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
|
||||||
return err
|
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 {
|
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
|
||||||
b := [8]byte{byte(role >> 8), byte(role), flags}
|
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 {
|
func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
|
||||||
b := make([]byte, 8)
|
b := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
binary.BigEndian.PutUint32(b, uint32(appStatus))
|
||||||
b[4] = protocolStatus
|
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 {
|
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
|
// Do made the request and returns a io.Reader that translates the data read
|
||||||
// from fcgi responder out of fcgi packet before returning it.
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.writePairs(FCGI_PARAMS, p)
|
err = c.writePairs(Params, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body := newWriter(c, FCGI_STDIN)
|
body := newWriter(c, Stdin)
|
||||||
if req != nil {
|
if req != nil {
|
||||||
io.Copy(body, req)
|
io.Copy(body, req)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,13 +34,13 @@ import (
|
||||||
// test failed if the remote fcgi(script) failed md5 verification
|
// test failed if the remote fcgi(script) failed md5 verification
|
||||||
// and output "FAILED" in response
|
// and output "FAILED" in response
|
||||||
const (
|
const (
|
||||||
script_file = "/tank/www/fcgic_test.php"
|
scriptFile = "/tank/www/fcgic_test.php"
|
||||||
//ip_port = "remote-php-serv:59000"
|
//ipPort = "remote-php-serv:59000"
|
||||||
ip_port = "127.0.0.1:59000"
|
ipPort = "127.0.0.1:59000"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
t_ *testing.T = nil
|
t_ *testing.T
|
||||||
)
|
)
|
||||||
|
|
||||||
type FastCGIServer struct{}
|
type FastCGIServer struct{}
|
||||||
|
@ -51,7 +51,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
stat := "PASSED"
|
stat := "PASSED"
|
||||||
fmt.Fprintln(resp, "-")
|
fmt.Fprintln(resp, "-")
|
||||||
file_num := 0
|
fileNum := 0
|
||||||
{
|
{
|
||||||
length := 0
|
length := 0
|
||||||
for k0, v0 := range req.Form {
|
for k0, v0 := range req.Form {
|
||||||
|
@ -69,7 +69,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.MultipartForm != nil {
|
if req.MultipartForm != nil {
|
||||||
file_num = len(req.MultipartForm.File)
|
fileNum = len(req.MultipartForm.File)
|
||||||
for kn, fns := range req.MultipartForm.File {
|
for kn, fns := range req.MultipartForm.File {
|
||||||
//fmt.Fprintln(resp, "server:filekey ", kn )
|
//fmt.Fprintln(resp, "server:filekey ", kn )
|
||||||
length += len(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, "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) {
|
func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
|
||||||
fcgi, err := Dial("tcp", ip_port)
|
fcgi, err := Dial("tcp", ipPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("err:", err)
|
log.Println("err:", err)
|
||||||
return
|
return
|
||||||
|
@ -119,16 +119,16 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
length = len(data)
|
length = len(data)
|
||||||
rd := bytes.NewReader(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 {
|
} else if len(posts) > 0 {
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
for k, v := range posts {
|
for k, v := range posts {
|
||||||
values.Set(k, v)
|
values.Set(k, v)
|
||||||
length += len(k) + 2 + len(v)
|
length += len(k) + 2 + len(v)
|
||||||
}
|
}
|
||||||
resp, err = fcgi.PostForm(fcgi_params, values)
|
resp, err = fcgi.PostForm(fcgiParams, values)
|
||||||
} else {
|
} else {
|
||||||
resp, err = fcgi.Get(fcgi_params)
|
resp, err = fcgi.Get(fcgiParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -142,7 +142,7 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
|
||||||
fi, _ := os.Lstat(v)
|
fi, _ := os.Lstat(v)
|
||||||
length += len(k) + int(fi.Size())
|
length += len(k) + int(fi.Size())
|
||||||
}
|
}
|
||||||
resp, err = fcgi.PostFile(fcgi_params, values, files)
|
resp, err = fcgi.PostFile(fcgiParams, values, files)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -191,7 +191,7 @@ func generateRandFile(size int) (p string, m string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func Disabled_Test(t *testing.T) {
|
func DisabledTest(t *testing.T) {
|
||||||
// TODO: test chunked reader
|
// TODO: test chunked reader
|
||||||
|
|
||||||
t_ = t
|
t_ = t
|
||||||
|
@ -199,7 +199,7 @@ func Disabled_Test(t *testing.T) {
|
||||||
|
|
||||||
// server
|
// server
|
||||||
go func() {
|
go func() {
|
||||||
listener, err := net.Listen("tcp", ip_port)
|
listener, err := net.Listen("tcp", ipPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// handle error
|
// handle error
|
||||||
log.Println("listener creatation failed: ", err)
|
log.Println("listener creatation failed: ", err)
|
||||||
|
@ -212,19 +212,19 @@ func Disabled_Test(t *testing.T) {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
// init
|
// init
|
||||||
fcgi_params := make(map[string]string)
|
fcgiParams := make(map[string]string)
|
||||||
fcgi_params["REQUEST_METHOD"] = "GET"
|
fcgiParams["REQUEST_METHOD"] = "GET"
|
||||||
fcgi_params["SERVER_PROTOCOL"] = "HTTP/1.1"
|
fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
|
||||||
//fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
|
//fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
|
||||||
fcgi_params["SCRIPT_FILENAME"] = script_file
|
fcgiParams["SCRIPT_FILENAME"] = scriptFile
|
||||||
|
|
||||||
// simple GET
|
// simple GET
|
||||||
log.Println("test:", "get")
|
log.Println("test:", "get")
|
||||||
sendFcgi(0, fcgi_params, nil, nil, nil)
|
sendFcgi(0, fcgiParams, nil, nil, nil)
|
||||||
|
|
||||||
// simple post data
|
// simple post data
|
||||||
log.Println("test:", "post")
|
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)")
|
log.Println("test:", "post data (more than 60KB)")
|
||||||
data := ""
|
data := ""
|
||||||
|
@ -240,13 +240,13 @@ func Disabled_Test(t *testing.T) {
|
||||||
|
|
||||||
data += k0 + "=" + url.QueryEscape(v0) + "&"
|
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)")
|
log.Println("test:", "post form (use url.Values)")
|
||||||
p0 := make(map[string]string, 1)
|
p0 := make(map[string]string, 1)
|
||||||
p0["c4ca4238a0b923820dcc509a6f75849b"] = "1"
|
p0["c4ca4238a0b923820dcc509a6f75849b"] = "1"
|
||||||
p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n"
|
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)")
|
log.Println("test:", "post forms (256 keys, more than 1MB)")
|
||||||
p1 := make(map[string]string, 1)
|
p1 := make(map[string]string, 1)
|
||||||
|
@ -257,25 +257,25 @@ func Disabled_Test(t *testing.T) {
|
||||||
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
k0 := fmt.Sprintf("%x", h.Sum(nil))
|
||||||
p1[k0] = v0
|
p1[k0] = v0
|
||||||
}
|
}
|
||||||
sendFcgi(1, fcgi_params, nil, p1, nil)
|
sendFcgi(1, fcgiParams, nil, p1, nil)
|
||||||
|
|
||||||
log.Println("test:", "post file (1 file, 500KB)) ")
|
log.Println("test:", "post file (1 file, 500KB)) ")
|
||||||
f0 := make(map[string]string, 1)
|
f0 := make(map[string]string, 1)
|
||||||
path0, m0 := generateRandFile(500000)
|
path0, m0 := generateRandFile(500000)
|
||||||
f0[m0] = path0
|
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")
|
log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data")
|
||||||
path1, m1 := generateRandFile(5000000)
|
path1, m1 := generateRandFile(5000000)
|
||||||
f0[m1] = path1
|
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)")
|
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")
|
log.Println("test:", "post only 1 file")
|
||||||
delete(f0, "m0")
|
delete(f0, "m0")
|
||||||
sendFcgi(1, fcgi_params, nil, nil, f0)
|
sendFcgi(1, fcgiParams, nil, nil, f0)
|
||||||
|
|
||||||
os.Remove(path0)
|
os.Remove(path0)
|
||||||
os.Remove(path1)
|
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
|
// elements in the set and passes each to f. It returns true
|
||||||
// on the first call to f that returns true and false otherwise.
|
// on the first call to f that returns true and false otherwise.
|
||||||
func (s Set) ContainsFunc(f func(string) bool) bool {
|
func (s Set) ContainsFunc(f func(string) bool) bool {
|
||||||
for k, _ := range s {
|
for k := range s {
|
||||||
if f(k) {
|
if f(k) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ func TestGzipHandler(t *testing.T) {
|
||||||
extFilter.Exts.Add(e)
|
extFilter.Exts.Add(e)
|
||||||
}
|
}
|
||||||
gz := Gzip{Configs: []Config{
|
gz := Gzip{Configs: []Config{
|
||||||
Config{Filters: []Filter{pathFilter, extFilter}},
|
{Filters: []Filter{pathFilter, extFilter}},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
|
@ -70,7 +70,7 @@ func generateLinks(md Markdown, cfg *Config) (bool, error) {
|
||||||
return generated, g.lastErr
|
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 {
|
func generateStaticHTML(md Markdown, cfg *Config) error {
|
||||||
// If generated site already exists, clear it out
|
// If generated site already exists, clear it out
|
||||||
_, err := os.Stat(cfg.StaticDir)
|
_, err := os.Stat(cfg.StaticDir)
|
||||||
|
@ -98,6 +98,7 @@ func generateStaticHTML(md Markdown, cfg *Config) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
reqPath = filepath.ToSlash(reqPath)
|
||||||
reqPath = "/" + reqPath
|
reqPath = "/" + reqPath
|
||||||
|
|
||||||
// Generate the static file
|
// Generate the static file
|
||||||
|
|
|
@ -22,7 +22,7 @@ func TestMarkdown(t *testing.T) {
|
||||||
Root: "./testdata",
|
Root: "./testdata",
|
||||||
FileSys: http.Dir("./testdata"),
|
FileSys: http.Dir("./testdata"),
|
||||||
Configs: []*Config{
|
Configs: []*Config{
|
||||||
&Config{
|
{
|
||||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||||
PathScope: "/blog",
|
PathScope: "/blog",
|
||||||
Extensions: []string{".md"},
|
Extensions: []string{".md"},
|
||||||
|
@ -32,7 +32,7 @@ func TestMarkdown(t *testing.T) {
|
||||||
StaticDir: DefaultStaticDir,
|
StaticDir: DefaultStaticDir,
|
||||||
StaticFiles: make(map[string]string),
|
StaticFiles: make(map[string]string),
|
||||||
},
|
},
|
||||||
&Config{
|
{
|
||||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||||
PathScope: "/log",
|
PathScope: "/log",
|
||||||
Extensions: []string{".md"},
|
Extensions: []string{".md"},
|
||||||
|
@ -42,7 +42,7 @@ func TestMarkdown(t *testing.T) {
|
||||||
StaticDir: DefaultStaticDir,
|
StaticDir: DefaultStaticDir,
|
||||||
StaticFiles: make(map[string]string),
|
StaticFiles: make(map[string]string),
|
||||||
},
|
},
|
||||||
&Config{
|
{
|
||||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||||
PathScope: "/og",
|
PathScope: "/og",
|
||||||
Extensions: []string{".md"},
|
Extensions: []string{".md"},
|
||||||
|
@ -52,7 +52,7 @@ func TestMarkdown(t *testing.T) {
|
||||||
StaticDir: "testdata/og_static",
|
StaticDir: "testdata/og_static",
|
||||||
StaticFiles: map[string]string{"/og/first.md": "testdata/og_static/og/first.md/index.html"},
|
StaticFiles: map[string]string{"/og/first.md": "testdata/og_static/og/first.md/index.html"},
|
||||||
Links: []PageLink{
|
Links: []PageLink{
|
||||||
PageLink{
|
{
|
||||||
Title: "first",
|
Title: "first",
|
||||||
Summary: "",
|
Summary: "",
|
||||||
Date: time.Now(),
|
Date: time.Now(),
|
||||||
|
|
|
@ -116,7 +116,7 @@ func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reqPath = "/" + reqPath
|
reqPath = "/" + filepath.ToSlash(reqPath)
|
||||||
|
|
||||||
parser := findParser(body)
|
parser := findParser(body)
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
|
|
|
@ -18,7 +18,7 @@ const (
|
||||||
DefaultStaticDir = "generated_site"
|
DefaultStaticDir = "generated_site"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarkdownData struct {
|
type Data struct {
|
||||||
middleware.Context
|
middleware.Context
|
||||||
Doc map[string]string
|
Doc map[string]string
|
||||||
Links []PageLink
|
Links []PageLink
|
||||||
|
@ -95,7 +95,7 @@ func (md Markdown) processTemplate(c *Config, requestPath string, tmpl []byte, m
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mdData := MarkdownData{
|
mdData := Data{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Doc: metadata.Variables,
|
Doc: metadata.Variables,
|
||||||
Links: c.Links,
|
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 it is index file, use the directory instead
|
||||||
if md.IsIndexFile(filepath.Base(requestPath)) {
|
if md.IsIndexFile(filepath.Base(requestPath)) {
|
||||||
|
@ -154,7 +157,7 @@ func (md Markdown) generatePage(c *Config, requestPath string, content []byte) e
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Lock()
|
c.Lock()
|
||||||
c.StaticFiles[requestPath] = filePath
|
c.StaticFiles[requestPath] = filepath.ToSlash(filePath)
|
||||||
c.Unlock()
|
c.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultInterval is the default interval at which the markdown watcher
|
||||||
|
// checks for changes.
|
||||||
const DefaultInterval = time.Second * 60
|
const DefaultInterval = time.Second * 60
|
||||||
|
|
||||||
// Watch monitors the configured markdown directory for changes. It calls GenerateLinks
|
// Watch monitors the configured markdown directory for changes. It calls GenerateLinks
|
||||||
|
|
|
@ -3,7 +3,7 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
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 '/'
|
// and false is returned. fpath must end in a forward slash '/'
|
||||||
// otherwise no index files will be tried (directory paths must end
|
// otherwise no index files will be tried (directory paths must end
|
||||||
// in a forward slash according to HTTP).
|
// 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) {
|
func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) {
|
||||||
if fpath[len(fpath)-1] != '/' || root == nil {
|
if fpath[len(fpath)-1] != '/' || root == nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
for _, indexFile := range indexFiles {
|
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)
|
f, err := root.Open(fp)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
|
@ -15,9 +15,12 @@ func TestIndexfile(t *testing.T) {
|
||||||
expectedBoolValue bool //return value
|
expectedBoolValue bool //return value
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
http.Dir("./templates/testdata"), "/images/", []string{"img.htm"},
|
http.Dir("./templates/testdata"),
|
||||||
|
"/images/",
|
||||||
|
[]string{"img.htm"},
|
||||||
false,
|
false,
|
||||||
"/images/img.htm", true,
|
"/images/img.htm",
|
||||||
|
true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
|
|
41
middleware/mime/mime.go
Normal file
41
middleware/mime/mime.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package mime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represent a mime config.
|
||||||
|
type Config struct {
|
||||||
|
Ext string
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContent sets the Content-Type header of the request if the request path
|
||||||
|
// is supported.
|
||||||
|
func (c Config) SetContent(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
ext := filepath.Ext(r.URL.Path)
|
||||||
|
if ext != c.Ext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", c.ContentType)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mime sets Content-Type header of requests based on configurations.
|
||||||
|
type Mime struct {
|
||||||
|
Next middleware.Handler
|
||||||
|
Configs []Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements the middleware.Handler interface.
|
||||||
|
func (e Mime) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
for _, c := range e.Configs {
|
||||||
|
if ok := c.SetContent(w, r); ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.Next.ServeHTTP(w, r)
|
||||||
|
}
|
75
middleware/mime/mime_test.go
Normal file
75
middleware/mime/mime_test.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package mime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMimeHandler(t *testing.T) {
|
||||||
|
|
||||||
|
mimes := map[string]string{
|
||||||
|
".html": "text/html",
|
||||||
|
".txt": "text/plain",
|
||||||
|
".swf": "application/x-shockwave-flash",
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs []Config
|
||||||
|
for ext, contentType := range mimes {
|
||||||
|
configs = append(configs, Config{Ext: ext, ContentType: contentType})
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Mime{Configs: configs}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
exts := []string{
|
||||||
|
".html", ".txt", ".swf",
|
||||||
|
}
|
||||||
|
for _, e := range exts {
|
||||||
|
url := "/file" + e
|
||||||
|
r, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
m.Next = nextFunc(true, mimes[e])
|
||||||
|
_, err = m.ServeHTTP(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
exts = []string{
|
||||||
|
".htm1", ".abc", ".mdx",
|
||||||
|
}
|
||||||
|
for _, e := range exts {
|
||||||
|
url := "/file" + e
|
||||||
|
r, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
m.Next = nextFunc(false, "")
|
||||||
|
_, err = m.ServeHTTP(w, r)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextFunc(shouldMime bool, contentType string) middleware.Handler {
|
||||||
|
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
if shouldMime {
|
||||||
|
if w.Header().Get("Content-Type") != contentType {
|
||||||
|
return 0, fmt.Errorf("expected Content-Type: %v, found %v", contentType, r.Header.Get("Content-Type"))
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if w.Header().Get("Content-Type") != "" {
|
||||||
|
return 0, fmt.Errorf("Content-Type header not expected")
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -12,13 +12,13 @@ func (r *customPolicy) Select(pool HostPool) *UpstreamHost {
|
||||||
|
|
||||||
func testPool() HostPool {
|
func testPool() HostPool {
|
||||||
pool := []*UpstreamHost{
|
pool := []*UpstreamHost{
|
||||||
&UpstreamHost{
|
{
|
||||||
Name: "http://google.com", // this should resolve (healthcheck test)
|
Name: "http://google.com", // this should resolve (healthcheck test)
|
||||||
},
|
},
|
||||||
&UpstreamHost{
|
{
|
||||||
Name: "http://shouldnot.resolve", // this shouldn't
|
Name: "http://shouldnot.resolve", // this shouldn't
|
||||||
},
|
},
|
||||||
&UpstreamHost{
|
{
|
||||||
Name: "http://C",
|
Name: "http://C",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
supportedPolicies map[string]func() Policy = make(map[string]func() Policy)
|
supportedPolicies = make(map[string]func() Policy)
|
||||||
proxyHeaders http.Header = make(http.Header)
|
proxyHeaders = make(http.Header)
|
||||||
)
|
)
|
||||||
|
|
||||||
type staticUpstream struct {
|
type staticUpstream struct {
|
||||||
|
@ -53,65 +53,9 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for c.NextBlock() {
|
for c.NextBlock() {
|
||||||
switch c.Val() {
|
if err := parseBlock(&c, upstream); err != nil {
|
||||||
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
|
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream.Hosts = make([]*UpstreamHost, len(to))
|
upstream.Hosts = make([]*UpstreamHost, len(to))
|
||||||
|
@ -165,6 +109,68 @@ func (u *staticUpstream) From() string {
|
||||||
return u.from
|
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() {
|
func (u *staticUpstream) healthCheck() {
|
||||||
for _, host := range u.Hosts {
|
for _, host := range u.Hosts {
|
||||||
hostURL := host.Name + u.HealthCheck.Path
|
hostURL := host.Name + u.HealthCheck.Path
|
||||||
|
|
|
@ -19,7 +19,7 @@ type Redirect struct {
|
||||||
// ServeHTTP implements the middleware.Handler interface.
|
// ServeHTTP implements the middleware.Handler interface.
|
||||||
func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
for _, rule := range rd.Rules {
|
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)
|
to := middleware.NewReplacer(r, nil, "").Replace(rule.To)
|
||||||
if rule.Meta {
|
if rule.Meta {
|
||||||
safeTo := html.EscapeString(to)
|
safeTo := html.EscapeString(to)
|
||||||
|
@ -33,9 +33,14 @@ func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
|
||||||
return rd.Next.ServeHTTP(w, r)
|
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.
|
// Rule describes an HTTP redirect rule.
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
From, To string
|
FromScheme, FromPath, To string
|
||||||
Code int
|
Code int
|
||||||
Meta bool
|
Meta bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ package redirect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mholt/caddy/middleware"
|
"github.com/mholt/caddy/middleware"
|
||||||
|
@ -14,15 +16,28 @@ func TestRedirect(t *testing.T) {
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
from string
|
from string
|
||||||
expectedLocation string
|
expectedLocation string
|
||||||
|
expectedCode int
|
||||||
}{
|
}{
|
||||||
{"/from", "/to"},
|
{"http://localhost/from", "/to", http.StatusMovedPermanently},
|
||||||
{"/a", "/b"},
|
{"http://localhost/a", "/b", http.StatusTemporaryRedirect},
|
||||||
{"/aa", ""},
|
{"http://localhost/aa", "", http.StatusOK},
|
||||||
{"/", ""},
|
{"http://localhost/", "", http.StatusOK},
|
||||||
{"/a?foo=bar", "/b"},
|
{"http://localhost/a?foo=bar", "/b", http.StatusTemporaryRedirect},
|
||||||
{"/asdf?foo=bar", ""},
|
{"http://localhost/asdf?foo=bar", "", http.StatusOK},
|
||||||
{"/foo#bar", ""},
|
{"http://localhost/foo#bar", "", http.StatusOK},
|
||||||
{"/a#foo", "/b"},
|
{"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
|
var nextCalled bool
|
||||||
|
|
||||||
|
@ -32,8 +47,16 @@ func TestRedirect(t *testing.T) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}),
|
}),
|
||||||
Rules: []Rule{
|
Rules: []Rule{
|
||||||
{From: "/from", To: "/to"},
|
{FromPath: "/from", To: "/to", Code: http.StatusMovedPermanently},
|
||||||
{From: "/a", To: "/b"},
|
{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 {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
|
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()
|
rec := httptest.NewRecorder()
|
||||||
re.ServeHTTP(rec, req)
|
re.ServeHTTP(rec, req)
|
||||||
|
@ -50,6 +76,11 @@ func TestRedirect(t *testing.T) {
|
||||||
i, test.expectedLocation, rec.Header().Get("Location"))
|
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 != "" {
|
if nextCalled && test.expectedLocation != "" {
|
||||||
t.Errorf("Test %d: Next handler was unexpectedly called", i)
|
t.Errorf("Test %d: Next handler was unexpectedly called", i)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +90,7 @@ func TestRedirect(t *testing.T) {
|
||||||
func TestParametersRedirect(t *testing.T) {
|
func TestParametersRedirect(t *testing.T) {
|
||||||
re := Redirect{
|
re := Redirect{
|
||||||
Rules: []Rule{
|
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{
|
re = Redirect{
|
||||||
Rules: []Rule{
|
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) {
|
func TestMetaRedirect(t *testing.T) {
|
||||||
re := Redirect{
|
re := Redirect{
|
||||||
Rules: []Rule{
|
Rules: []Rule{
|
||||||
{From: "/whatever", Meta: true, To: "/something"},
|
{FromPath: "/whatever", Meta: true, To: "/something"},
|
||||||
{From: "/", Meta: true, To: "https://example.com/"},
|
{FromPath: "/", Meta: true, To: "https://example.com/"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range re.Rules {
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
|
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
func TestNewReplacer(t *testing.T) {
|
func TestNewReplacer(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
recordRequest := NewResponseRecorder(w)
|
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
|
request, err := http.NewRequest("POST", "http://caddyserver.com", reader) //Create request with JSON body
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -41,9 +41,9 @@ func TestNewReplacer(t *testing.T) {
|
||||||
func TestReplace(t *testing.T) {
|
func TestReplace(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
recordRequest := NewResponseRecorder(w)
|
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
|
request, err := http.NewRequest("POST", "http://caddyserver.com", reader) //Create request with JSON body
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -49,6 +49,9 @@ func NewSimpleRule(from, to string) SimpleRule {
|
||||||
// Rewrite rewrites the internal location of the current request.
|
// Rewrite rewrites the internal location of the current request.
|
||||||
func (s SimpleRule) Rewrite(r *http.Request) bool {
|
func (s SimpleRule) Rewrite(r *http.Request) bool {
|
||||||
if s.From == r.URL.Path {
|
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
|
r.URL.Path = s.To
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -112,7 +115,7 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool {
|
||||||
// include trailing slash in regexp if present
|
// include trailing slash in regexp if present
|
||||||
start := len(r.Base)
|
start := len(r.Base)
|
||||||
if strings.HasSuffix(r.Base, "/") {
|
if strings.HasSuffix(r.Base, "/") {
|
||||||
start -= 1
|
start--
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate regexp
|
// validate regexp
|
||||||
|
@ -129,6 +132,10 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool {
|
||||||
return false
|
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
|
// perform rewrite
|
||||||
req.URL.Path = url.Path
|
req.URL.Path = url.Path
|
||||||
if url.RawQuery != "" {
|
if url.RawQuery != "" {
|
||||||
|
@ -169,3 +176,8 @@ func (r *RegexpRule) matchExt(rPath string) bool {
|
||||||
}
|
}
|
||||||
return true
|
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{
|
regexpRules := [][]string{
|
||||||
[]string{"/reg/", ".*", "/to", ""},
|
{"/reg/", ".*", "/to", ""},
|
||||||
[]string{"/r/", "[a-z]+", "/toaz", "!.html|"},
|
{"/r/", "[a-z]+", "/toaz", "!.html|"},
|
||||||
[]string{"/url/", "a([a-z0-9]*)s([A-Z]{2})", "/to/{path}", ""},
|
{"/url/", "a([a-z0-9]*)s([A-Z]{2})", "/to/{path}", ""},
|
||||||
[]string{"/ab/", "ab", "/ab?{query}", ".txt|"},
|
{"/ab/", "ab", "/ab?{query}", ".txt|"},
|
||||||
[]string{"/ab/", "ab", "/ab?type=html&{query}", ".html|"},
|
{"/ab/", "ab", "/ab?type=html&{query}", ".html|"},
|
||||||
[]string{"/abc/", "ab", "/abc/{file}", ".html|"},
|
{"/abc/", "ab", "/abc/{file}", ".html|"},
|
||||||
[]string{"/abcd/", "ab", "/a/{dir}/{file}", ".html|"},
|
{"/abcd/", "ab", "/a/{dir}/{file}", ".html|"},
|
||||||
[]string{"/abcde/", "ab", "/a#{fragment}", ".html|"},
|
{"/abcde/", "ab", "/a#{fragment}", ".html|"},
|
||||||
[]string{"/ab/", `.*\.jpg`, "/ajpg", ""},
|
{"/ab/", `.*\.jpg`, "/ajpg", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, regexpRule := range regexpRules {
|
for _, regexpRule := range regexpRules {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LogRoller implements a middleware that provides a rolling logger.
|
||||||
type LogRoller struct {
|
type LogRoller struct {
|
||||||
Filename string
|
Filename string
|
||||||
MaxSize int
|
MaxSize int
|
||||||
|
@ -14,6 +15,7 @@ type LogRoller struct {
|
||||||
LocalTime bool
|
LocalTime bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLogWriter returns an io.Writer that writes to a rolling logger.
|
||||||
func (l LogRoller) GetLogWriter() io.Writer {
|
func (l LogRoller) GetLogWriter() io.Writer {
|
||||||
return &lumberjack.Logger{
|
return &lumberjack.Logger{
|
||||||
Filename: l.Filename,
|
Filename: l.Filename,
|
||||||
|
|
|
@ -14,12 +14,12 @@ func Test(t *testing.T) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}),
|
}),
|
||||||
Rules: []Rule{
|
Rules: []Rule{
|
||||||
Rule{
|
{
|
||||||
Extensions: []string{".html"},
|
Extensions: []string{".html"},
|
||||||
IndexFiles: []string{"index.html"},
|
IndexFiles: []string{"index.html"},
|
||||||
Path: "/photos",
|
Path: "/photos",
|
||||||
},
|
},
|
||||||
Rule{
|
{
|
||||||
Extensions: []string{".html", ".htm"},
|
Extensions: []string{".html", ".htm"},
|
||||||
IndexFiles: []string{"index.html", "index.htm"},
|
IndexFiles: []string{"index.html", "index.htm"},
|
||||||
Path: "/images",
|
Path: "/images",
|
||||||
|
@ -34,7 +34,7 @@ func Test(t *testing.T) {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}),
|
}),
|
||||||
Rules: []Rule{
|
Rules: []Rule{
|
||||||
Rule{
|
{
|
||||||
Extensions: []string{".html"},
|
Extensions: []string{".html"},
|
||||||
IndexFiles: []string{"index.html"},
|
IndexFiles: []string{"index.html"},
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
|
229
middleware/websocket/websocket.go
Normal file
229
middleware/websocket/websocket.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
// Package websocket implements a WebSocket server by executing
|
||||||
|
// a command and piping its input and output through the WebSocket
|
||||||
|
// connection.
|
||||||
|
package websocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Time allowed to write a message to the peer.
|
||||||
|
writeWait = 10 * time.Second
|
||||||
|
|
||||||
|
// Time allowed to read the next pong message from the peer.
|
||||||
|
pongWait = 60 * time.Second
|
||||||
|
|
||||||
|
// Send pings to peer with this period. Must be less than pongWait.
|
||||||
|
pingPeriod = (pongWait * 9) / 10
|
||||||
|
|
||||||
|
// Maximum message size allowed from peer.
|
||||||
|
maxMessageSize = 1024 * 1024 * 10 // 10 MB default.
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// GatewayInterface is the dialect of CGI being used by the server
|
||||||
|
// to communicate with the script. See CGI spec, 4.1.4
|
||||||
|
GatewayInterface string
|
||||||
|
|
||||||
|
// ServerSoftware is the name and version of the information server
|
||||||
|
// software making the CGI request. See CGI spec, 4.1.17
|
||||||
|
ServerSoftware string
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// WebSocket is a type that holds configuration for the
|
||||||
|
// websocket middleware generally, like a list of all the
|
||||||
|
// websocket endpoints.
|
||||||
|
WebSocket struct {
|
||||||
|
// Next is the next HTTP handler in the chain for when the path doesn't match
|
||||||
|
Next middleware.Handler
|
||||||
|
|
||||||
|
// Sockets holds all the web socket endpoint configurations
|
||||||
|
Sockets []Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the configuration for a single websocket
|
||||||
|
// endpoint which may serve multiple websocket connections.
|
||||||
|
Config struct {
|
||||||
|
Path string
|
||||||
|
Command string
|
||||||
|
Arguments []string
|
||||||
|
Respawn bool // TODO: Not used, but parser supports it until we decide on it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
|
||||||
|
func (ws WebSocket) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
for _, sockconfig := range ws.Sockets {
|
||||||
|
if middleware.Path(r.URL.Path).Matches(sockconfig.Path) {
|
||||||
|
return serveWS(w, r, &sockconfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't match a websocket path, so pass-thru
|
||||||
|
return ws.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveWS is used for setting and upgrading the HTTP connection to a websocket connection.
|
||||||
|
// It also spawns the child process that is associated with matched HTTP path/url.
|
||||||
|
func serveWS(w http.ResponseWriter, r *http.Request, config *Config) (int, error) {
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadRequest, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command(config.Command, config.Arguments...)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadGateway, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadGateway, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metavars, err := buildEnv(cmd.Path, r)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusBadGateway, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Env = metavars
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return http.StatusBadGateway, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader(conn, stdout, stdin)
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildEnv creates the meta-variables for the child process according
|
||||||
|
// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1
|
||||||
|
// cmdPath should be the path of the command being run.
|
||||||
|
// The returned string slice can be set to the command's Env property.
|
||||||
|
func buildEnv(cmdPath string, r *http.Request) (metavars []string, err error) {
|
||||||
|
remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverHost, serverPort, err := net.SplitHostPort(r.Host)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metavars = []string{
|
||||||
|
`AUTH_TYPE=`, // Not used
|
||||||
|
`CONTENT_LENGTH=`, // Not used
|
||||||
|
`CONTENT_TYPE=`, // Not used
|
||||||
|
`GATEWAY_INTERFACE=` + GatewayInterface,
|
||||||
|
`PATH_INFO=`, // TODO
|
||||||
|
`PATH_TRANSLATED=`, // TODO
|
||||||
|
`QUERY_STRING=` + r.URL.RawQuery,
|
||||||
|
`REMOTE_ADDR=` + remoteHost,
|
||||||
|
`REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them
|
||||||
|
`REMOTE_IDENT=`, // Not used
|
||||||
|
`REMOTE_PORT=` + remotePort,
|
||||||
|
`REMOTE_USER=`, // Not used,
|
||||||
|
`REQUEST_METHOD=` + r.Method,
|
||||||
|
`REQUEST_URI=` + r.RequestURI,
|
||||||
|
`SCRIPT_NAME=` + cmdPath, // path of the program being executed
|
||||||
|
`SERVER_NAME=` + serverHost,
|
||||||
|
`SERVER_PORT=` + serverPort,
|
||||||
|
`SERVER_PROTOCOL=` + r.Proto,
|
||||||
|
`SERVER_SOFTWARE=` + ServerSoftware,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each HTTP header to the environment as well
|
||||||
|
for header, values := range r.Header {
|
||||||
|
value := strings.Join(values, ", ")
|
||||||
|
header = strings.ToUpper(header)
|
||||||
|
header = strings.Replace(header, "-", "_", -1)
|
||||||
|
value = strings.Replace(value, "\n", " ", -1)
|
||||||
|
metavars = append(metavars, "HTTP_"+header+"="+value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reader is the guts of this package. It takes the stdin and stdout pipes
|
||||||
|
// of the cmd we created in ServeWS and pipes them between the client and server
|
||||||
|
// over websockets.
|
||||||
|
func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) {
|
||||||
|
// Setup our connection's websocket ping/pong handlers from our const values.
|
||||||
|
conn.SetReadLimit(maxMessageSize)
|
||||||
|
conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||||
|
tickerChan := make(chan bool)
|
||||||
|
defer func() { tickerChan <- true }() // make sure to close the ticker when we are done.
|
||||||
|
go ticker(conn, tickerChan)
|
||||||
|
|
||||||
|
for {
|
||||||
|
msgType, r, err := conn.NextReader()
|
||||||
|
if err != nil {
|
||||||
|
if msgType == -1 {
|
||||||
|
return // we got a disconnect from the client. We are good to close.
|
||||||
|
}
|
||||||
|
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := conn.NextWriter(msgType)
|
||||||
|
if err != nil {
|
||||||
|
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(stdin, r); err != nil {
|
||||||
|
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if _, err := io.Copy(w, stdout); err != nil {
|
||||||
|
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""), time.Time{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ticker is start by the reader. Basically it is the method that simulates the websocket
|
||||||
|
// between the server and client to keep it alive with ping messages.
|
||||||
|
func ticker(conn *websocket.Conn, c chan bool) {
|
||||||
|
ticker := time.NewTicker(pingPeriod)
|
||||||
|
defer func() {
|
||||||
|
ticker.Stop()
|
||||||
|
close(c)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for { // blocking loop with select to wait for stimulation.
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
conn.WriteMessage(websocket.PingMessage, nil)
|
||||||
|
case <-c:
|
||||||
|
return // clean up this routine.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,89 +0,0 @@
|
||||||
package websockets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WebSocket represents a web socket server instance. A WebSocket
|
|
||||||
// is instantiated for each new websocket request/connection.
|
|
||||||
type WebSocket struct {
|
|
||||||
Config
|
|
||||||
*http.Request
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle handles a WebSocket connection. It launches the
|
|
||||||
// specified command and streams input and output through
|
|
||||||
// the command's stdin and stdout.
|
|
||||||
func (ws WebSocket) Handle(conn *websocket.Conn) {
|
|
||||||
cmd := exec.Command(ws.Command, ws.Arguments...)
|
|
||||||
|
|
||||||
cmd.Stdin = conn
|
|
||||||
cmd.Stdout = conn
|
|
||||||
cmd.Stderr = conn // TODO: Make this configurable from the Caddyfile
|
|
||||||
|
|
||||||
metavars, err := ws.buildEnv(cmd.Path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err) // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = metavars
|
|
||||||
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildEnv creates the meta-variables for the child process according
|
|
||||||
// to the CGI 1.1 specification: http://tools.ietf.org/html/rfc3875#section-4.1
|
|
||||||
// cmdPath should be the path of the command being run.
|
|
||||||
// The returned string slice can be set to the command's Env property.
|
|
||||||
func (ws WebSocket) buildEnv(cmdPath string) (metavars []string, err error) {
|
|
||||||
remoteHost, remotePort, err := net.SplitHostPort(ws.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serverHost, serverPort, err := net.SplitHostPort(ws.Host)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
metavars = []string{
|
|
||||||
`AUTH_TYPE=`, // Not used
|
|
||||||
`CONTENT_LENGTH=`, // Not used
|
|
||||||
`CONTENT_TYPE=`, // Not used
|
|
||||||
`GATEWAY_INTERFACE=` + GatewayInterface,
|
|
||||||
`PATH_INFO=`, // TODO
|
|
||||||
`PATH_TRANSLATED=`, // TODO
|
|
||||||
`QUERY_STRING=` + ws.URL.RawQuery,
|
|
||||||
`REMOTE_ADDR=` + remoteHost,
|
|
||||||
`REMOTE_HOST=` + remoteHost, // Host lookups are slow - don't do them
|
|
||||||
`REMOTE_IDENT=`, // Not used
|
|
||||||
`REMOTE_PORT=` + remotePort,
|
|
||||||
`REMOTE_USER=`, // Not used,
|
|
||||||
`REQUEST_METHOD=` + ws.Method,
|
|
||||||
`REQUEST_URI=` + ws.RequestURI,
|
|
||||||
`SCRIPT_NAME=` + cmdPath, // path of the program being executed
|
|
||||||
`SERVER_NAME=` + serverHost,
|
|
||||||
`SERVER_PORT=` + serverPort,
|
|
||||||
`SERVER_PROTOCOL=` + ws.Proto,
|
|
||||||
`SERVER_SOFTWARE=` + ServerSoftware,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add each HTTP header to the environment as well
|
|
||||||
for header, values := range ws.Header {
|
|
||||||
value := strings.Join(values, ", ")
|
|
||||||
header = strings.ToUpper(header)
|
|
||||||
header = strings.Replace(header, "-", "_", -1)
|
|
||||||
value = strings.Replace(value, "\n", " ", -1)
|
|
||||||
metavars = append(metavars, "HTTP_"+header+"="+value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
// Package websockets implements a WebSocket server by executing
|
|
||||||
// a command and piping its input and output through the WebSocket
|
|
||||||
// connection.
|
|
||||||
package websockets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/mholt/caddy/middleware"
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// WebSockets is a type that holds configuration for the
|
|
||||||
// websocket middleware generally, like a list of all the
|
|
||||||
// websocket endpoints.
|
|
||||||
WebSockets struct {
|
|
||||||
// Next is the next HTTP handler in the chain for when the path doesn't match
|
|
||||||
Next middleware.Handler
|
|
||||||
|
|
||||||
// Sockets holds all the web socket endpoint configurations
|
|
||||||
Sockets []Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config holds the configuration for a single websocket
|
|
||||||
// endpoint which may serve multiple websocket connections.
|
|
||||||
Config struct {
|
|
||||||
Path string
|
|
||||||
Command string
|
|
||||||
Arguments []string
|
|
||||||
Respawn bool // TODO: Not used, but parser supports it until we decide on it
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServeHTTP converts the HTTP request to a WebSocket connection and serves it up.
|
|
||||||
func (ws WebSockets) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
|
||||||
for _, sockconfig := range ws.Sockets {
|
|
||||||
if middleware.Path(r.URL.Path).Matches(sockconfig.Path) {
|
|
||||||
socket := WebSocket{
|
|
||||||
Config: sockconfig,
|
|
||||||
Request: r,
|
|
||||||
}
|
|
||||||
websocket.Handler(socket.Handle).ServeHTTP(w, r)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Didn't match a websocket path, so pass-thru
|
|
||||||
return ws.Next.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// GatewayInterface is the dialect of CGI being used by the server
|
|
||||||
// to communicate with the script. See CGI spec, 4.1.4
|
|
||||||
GatewayInterface string
|
|
||||||
|
|
||||||
// ServerSoftware is the name and version of the information server
|
|
||||||
// software making the CGI request. See CGI spec, 4.1.17
|
|
||||||
ServerSoftware string
|
|
||||||
)
|
|
|
@ -86,7 +86,7 @@ func (s *Server) Serve() error {
|
||||||
go func(vh virtualHost) {
|
go func(vh virtualHost) {
|
||||||
// Wait for signal
|
// Wait for signal
|
||||||
interrupt := make(chan os.Signal, 1)
|
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
|
<-interrupt
|
||||||
|
|
||||||
// Run callbacks
|
// 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) {
|
func DefaultErrorFunc(w http.ResponseWriter, r *http.Request, status int) {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
fmt.Fprintf(w, "%d %s", status, http.StatusText(status))
|
fmt.Fprintf(w, "%d %s", status, http.StatusText(status))
|
||||||
|
|
Loading…
Reference in a new issue