Commit ad057ab8 authored by Matthew Holt's avatar Matthew Holt

Merge branch 'master' into letsencrypt

Conflicts:
	caddy/parse/parse.go
	caddy/parse/parsing.go
	config/config.go
	config/setup/controller.go
	main.go
	server/server.go
parents c3e64636 09341fca
...@@ -9,7 +9,6 @@ environment: ...@@ -9,7 +9,6 @@ environment:
install: install:
- go get golang.org/x/tools/cmd/vet - go get golang.org/x/tools/cmd/vet
- echo %PATH%
- echo %GOPATH% - echo %GOPATH%
- go version - go version
- go env - go env
......
package caddy package caddy
import ( import (
"reflect"
"sync"
"testing" "testing"
"github.com/mholt/caddy/server" "github.com/mholt/caddy/server"
) )
func TestNewDefault(t *testing.T) {
config := NewDefault()
if actual, expected := config.Root, DefaultRoot; actual != expected {
t.Errorf("Root was %s but expected %s", actual, expected)
}
if actual, expected := config.Host, DefaultHost; actual != expected {
t.Errorf("Host was %s but expected %s", actual, expected)
}
if actual, expected := config.Port, DefaultPort; actual != expected {
t.Errorf("Port was %s but expected %s", actual, expected)
}
}
func TestResolveAddr(t *testing.T) { func TestResolveAddr(t *testing.T) {
// NOTE: If tests fail due to comparing to string "127.0.0.1", // NOTE: If tests fail due to comparing to string "127.0.0.1",
// it's possible that system env resolves with IPv6, or ::1. // it's possible that system env resolves with IPv6, or ::1.
...@@ -62,3 +78,61 @@ func TestResolveAddr(t *testing.T) { ...@@ -62,3 +78,61 @@ func TestResolveAddr(t *testing.T) {
} }
} }
} }
func TestMakeOnces(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
onces := makeOnces()
if len(onces) != len(directives) {
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
}
expected := map[string]*sync.Once{
"dummy": new(sync.Once),
"dummy2": new(sync.Once),
}
if !reflect.DeepEqual(onces, expected) {
t.Errorf("onces was %v, expected %v", onces, expected)
}
}
func TestMakeStorages(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
storages := makeStorages()
if len(storages) != len(directives) {
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
}
expected := map[string]interface{}{
"dummy": nil,
"dummy2": nil,
}
if !reflect.DeepEqual(storages, expected) {
t.Errorf("storages was %v, expected %v", storages, expected)
}
}
func TestValidDirective(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
for i, test := range []struct {
directive string
valid bool
}{
{"dummy", true},
{"dummy2", true},
{"dummy3", false},
} {
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
}
}
}
...@@ -58,6 +58,9 @@ func NewTestController(input string) *Controller { ...@@ -58,6 +58,9 @@ func NewTestController(input string) *Controller {
Root: ".", Root: ".",
}, },
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
OncePerServerBlock: func(f func() error) error {
return f()
},
} }
} }
......
package setup
import (
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
)
// The Startup function's tests are symmetrical to Shutdown tests,
// because the Startup and Shutdown functions share virtually the
// same functionality
func TestStartup(t *testing.T) {
tempDirPath, err := getTempDirPath()
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown.go")
osSenitiveTestDir := filepath.FromSlash(testDir)
exec.Command("rm", "-r", osSenitiveTestDir).Run() // removes osSenitiveTestDir from the OS's temp directory, if the osSenitiveTestDir already exists
tests := []struct {
input string
shouldExecutionErr bool
shouldRemoveErr bool
}{
// test case #0 tests proper functionality blocking commands
{"startup mkdir " + osSenitiveTestDir, false, false},
// test case #1 tests proper functionality of non-blocking commands
{"startup mkdir " + osSenitiveTestDir + " &", false, true},
// test case #2 tests handling of non-existant commands
{"startup " + strconv.Itoa(int(time.Now().UnixNano())), true, true},
}
for i, test := range tests {
c := NewTestController(test.input)
_, err = Startup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
err = c.Startup[0]()
if err != nil && !test.shouldExecutionErr {
t.Errorf("Test %d recieved an error of:\n%v", i, err)
}
err = os.Remove(osSenitiveTestDir)
if err != nil && !test.shouldRemoveErr {
t.Errorf("Test %d recieved an error of:\n%v", i, err)
}
}
}
...@@ -32,18 +32,48 @@ func templatesParse(c *Controller) ([]templates.Rule, error) { ...@@ -32,18 +32,48 @@ func templatesParse(c *Controller) ([]templates.Rule, error) {
for c.Next() { for c.Next() {
var rule templates.Rule var rule templates.Rule
if c.NextArg() { rule.Path = defaultTemplatePath
rule.Extensions = defaultTemplateExtensions
args := c.RemainingArgs()
switch len(args) {
case 0:
// Optional block
for c.NextBlock() {
switch c.Val() {
case "path":
args := c.RemainingArgs()
if len(args) != 1 {
return nil, c.ArgErr()
}
rule.Path = args[0]
case "ext":
args := c.RemainingArgs()
if len(args) == 0 {
return nil, c.ArgErr()
}
rule.Extensions = args
case "between":
args := c.RemainingArgs()
if len(args) != 2 {
return nil, c.ArgErr()
}
rule.Delims[0] = args[0]
rule.Delims[1] = args[1]
}
}
default:
// First argument would be the path // First argument would be the path
rule.Path = c.Val() rule.Path = args[0]
// Any remaining arguments are extensions // Any remaining arguments are extensions
rule.Extensions = c.RemainingArgs() rule.Extensions = args[1:]
if len(rule.Extensions) == 0 { if len(rule.Extensions) == 0 {
rule.Extensions = defaultTemplateExtensions rule.Extensions = defaultTemplateExtensions
} }
} else {
rule.Path = defaultTemplatePath
rule.Extensions = defaultTemplateExtensions
} }
for _, ext := range rule.Extensions { for _, ext := range rule.Extensions {
...@@ -52,7 +82,6 @@ func templatesParse(c *Controller) ([]templates.Rule, error) { ...@@ -52,7 +82,6 @@ func templatesParse(c *Controller) ([]templates.Rule, error) {
rules = append(rules, rule) rules = append(rules, rule)
} }
return rules, nil return rules, nil
} }
......
...@@ -2,8 +2,9 @@ package setup ...@@ -2,8 +2,9 @@ package setup
import ( import (
"fmt" "fmt"
"github.com/mholt/caddy/middleware/templates"
"testing" "testing"
"github.com/mholt/caddy/middleware/templates"
) )
func TestTemplates(t *testing.T) { func TestTemplates(t *testing.T) {
...@@ -40,7 +41,11 @@ func TestTemplates(t *testing.T) { ...@@ -40,7 +41,11 @@ func TestTemplates(t *testing.T) {
if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) { if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
t.Errorf("Expected %v to be the Default Index files", indexFiles) t.Errorf("Expected %v to be the Default Index files", indexFiles)
} }
if myHandler.Rules[0].Delims != [2]string{} {
t.Errorf("Expected %v to be the Default Delims", [2]string{})
}
} }
func TestTemplatesParse(t *testing.T) { func TestTemplatesParse(t *testing.T) {
tests := []struct { tests := []struct {
inputTemplateConfig string inputTemplateConfig string
...@@ -50,19 +55,32 @@ func TestTemplatesParse(t *testing.T) { ...@@ -50,19 +55,32 @@ func TestTemplatesParse(t *testing.T) {
{`templates /api1`, false, []templates.Rule{{ {`templates /api1`, false, []templates.Rule{{
Path: "/api1", Path: "/api1",
Extensions: defaultTemplateExtensions, Extensions: defaultTemplateExtensions,
Delims: [2]string{},
}}}, }}},
{`templates /api2 .txt .htm`, false, []templates.Rule{{ {`templates /api2 .txt .htm`, false, []templates.Rule{{
Path: "/api2", Path: "/api2",
Extensions: []string{".txt", ".htm"}, Extensions: []string{".txt", ".htm"},
Delims: [2]string{},
}}}, }}},
{`templates /api3 .htm .html {`templates /api3 .htm .html
templates /api4 .txt .tpl `, false, []templates.Rule{{ templates /api4 .txt .tpl `, false, []templates.Rule{{
Path: "/api3", Path: "/api3",
Extensions: []string{".htm", ".html"}, Extensions: []string{".htm", ".html"},
Delims: [2]string{},
}, { }, {
Path: "/api4", Path: "/api4",
Extensions: []string{".txt", ".tpl"}, Extensions: []string{".txt", ".tpl"},
Delims: [2]string{},
}}},
{`templates {
path /api5
ext .html
between {% %}
}`, false, []templates.Rule{{
Path: "/api5",
Extensions: []string{".html"},
Delims: [2]string{"{%", "%}"},
}}}, }}},
} }
for i, test := range tests { for i, test := range tests {
......
...@@ -16,6 +16,12 @@ func TLS(c *Controller) (middleware.Middleware, error) { ...@@ -16,6 +16,12 @@ func TLS(c *Controller) (middleware.Middleware, error) {
"specify port 80 explicitly (https://%s:80).", c.Port, c.Host, c.Host) "specify port 80 explicitly (https://%s:80).", c.Port, c.Host, c.Host)
} }
if c.Port == "http" {
c.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).", c.Port, c.Host, c.Host)
}
for c.Next() { for c.Next() {
args := c.RemainingArgs() args := c.RemainingArgs()
switch len(args) { switch len(args) {
......
...@@ -54,6 +54,25 @@ func TestWebSocketParse(t *testing.T) { ...@@ -54,6 +54,25 @@ func TestWebSocketParse(t *testing.T) {
Path: "/api4", Path: "/api4",
Command: "cat", Command: "cat",
}}}, }}},
{`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{
Path: "/api5",
Command: "cmd",
Arguments: []string{"arg1", "arg2", "arg3"},
}}},
// accept respawn
{`websocket /api6 cat {
respawn
}`, false, []websocket.Config{{
Path: "/api6",
Command: "cat",
}}},
// invalid configuration
{`websocket /api7 cat {
invalid
}`, true, []websocket.Config{}},
} }
for i, test := range tests { for i, test := range tests {
c := NewTestController(test.inputWebSocketConfig) c := NewTestController(test.inputWebSocketConfig)
......
package main
import (
"runtime"
"testing"
)
func TestSetCPU(t *testing.T) {
currentCPU := runtime.GOMAXPROCS(-1)
maxCPU := runtime.NumCPU()
for i, test := range []struct {
input string
output int
shouldErr bool
}{
{"1", 1, false},
{"-1", currentCPU, true},
{"0", currentCPU, true},
{"100%", maxCPU, false},
{"50%", int(0.5 * float32(maxCPU)), false},
{"110%", currentCPU, true},
{"-10%", currentCPU, true},
{"invalid input", currentCPU, true},
{"invalid input%", currentCPU, true},
{"9999", maxCPU, false}, // over available CPU
} {
err := setCPU(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
}
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
}
// teardown
runtime.GOMAXPROCS(currentCPU)
}
}
...@@ -2,18 +2,30 @@ package middleware ...@@ -2,18 +2,30 @@ package middleware
import ( import (
"errors" "errors"
"runtime"
"unicode"
"github.com/flynn/go-shlex" "github.com/flynn/go-shlex"
) )
var runtimeGoos = runtime.GOOS
// SplitCommandAndArgs takes a command string and parses it // SplitCommandAndArgs takes a command string and parses it
// shell-style into the command and its separate arguments. // shell-style into the command and its separate arguments.
func SplitCommandAndArgs(command string) (cmd string, args []string, err error) { func SplitCommandAndArgs(command string) (cmd string, args []string, err error) {
parts, err := shlex.Split(command) var parts []string
if err != nil {
err = errors.New("error parsing command: " + err.Error()) if runtimeGoos == "windows" {
return parts = parseWindowsCommand(command) // parse it Windows-style
} else if len(parts) == 0 { } else {
parts, err = parseUnixCommand(command) // parse it Unix-style
if err != nil {
err = errors.New("error parsing command: " + err.Error())
return
}
}
if len(parts) == 0 {
err = errors.New("no command contained in '" + command + "'") err = errors.New("no command contained in '" + command + "'")
return return
} }
...@@ -25,3 +37,84 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error) ...@@ -25,3 +37,84 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error)
return return
} }
// parseUnixCommand parses a unix style command line and returns the
// command and its arguments or an error
func parseUnixCommand(cmd string) ([]string, error) {
return shlex.Split(cmd)
}
// parseWindowsCommand parses windows command lines and
// returns the command and the arguments as an array. It
// should be able to parse commonly used command lines.
// Only basic syntax is supported:
// - spaces in double quotes are not token delimiters
// - double quotes are escaped by either backspace or another double quote
// - except for the above case backspaces are path separators (not special)
//
// Many sources point out that escaping quotes using backslash can be unsafe.
// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 )
//
// This function has to be used on Windows instead
// of the shlex package because this function treats backslash
// characters properly.
func parseWindowsCommand(cmd string) []string {
const backslash = '\\'
const quote = '"'
var parts []string
var part string
var inQuotes bool
var lastRune rune
for i, ch := range cmd {
if i != 0 {
lastRune = rune(cmd[i-1])
}
if ch == backslash {
// put it in the part - for now we don't know if it's an
// escaping char or path separator
part += string(ch)
continue
}
if ch == quote {
if lastRune == backslash {
// remove the backslash from the part and add the escaped quote instead
part = part[:len(part)-1]
part += string(ch)
continue
}
if lastRune == quote {
// revert the last change of the inQuotes state
// it was an escaping quote
inQuotes = !inQuotes
part += string(ch)
continue
}
// normal escaping quotes
inQuotes = !inQuotes
continue
}
if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 {
parts = append(parts, part)
part = ""
continue
}
part += string(ch)
}
if len(part) > 0 {
parts = append(parts, part)
part = ""
}
return parts
}
...@@ -2,11 +2,176 @@ package middleware ...@@ -2,11 +2,176 @@ package middleware
import ( import (
"fmt" "fmt"
"runtime"
"strings" "strings"
"testing" "testing"
) )
func TestParseUnixCommand(t *testing.T) {
tests := []struct {
input string
expected []string
}{
// 0 - emtpy command
{
input: ``,
expected: []string{},
},
// 1 - command without arguments
{
input: `command`,
expected: []string{`command`},
},
// 2 - command with single argument
{
input: `command arg1`,
expected: []string{`command`, `arg1`},
},
// 3 - command with multiple arguments
{
input: `command arg1 arg2`,
expected: []string{`command`, `arg1`, `arg2`},
},
// 4 - command with single argument with space character - in quotes
{
input: `command "arg1 arg1"`,
expected: []string{`command`, `arg1 arg1`},
},
// 5 - command with multiple spaces and tab character
{
input: "command arg1 arg2\targ3",
expected: []string{`command`, `arg1`, `arg2`, `arg3`},
},
// 6 - command with single argument with space character - escaped with backspace
{
input: `command arg1\ arg2`,
expected: []string{`command`, `arg1 arg2`},
},
// 7 - single quotes should escape special chars
{
input: `command 'arg1\ arg2'`,
expected: []string{`command`, `arg1\ arg2`},
},
}
for i, test := range tests {
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
actual, _ := parseUnixCommand(test.input)
if len(actual) != len(test.expected) {
t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual)
continue
}
for j := 0; j < len(actual); j++ {
if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart {
t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j)
}
}
}
}
func TestParseWindowsCommand(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{ // 0 - empty command - do not fail
input: ``,
expected: []string{},
},
{ // 1 - cmd without args
input: `cmd`,
expected: []string{`cmd`},
},
{ // 2 - multiple args
input: `cmd arg1 arg2`,
expected: []string{`cmd`, `arg1`, `arg2`},
},
{ // 3 - multiple args with space
input: `cmd "combined arg" arg2`,
expected: []string{`cmd`, `combined arg`, `arg2`},
},
{ // 4 - path without spaces
input: `mkdir C:\Windows\foo\bar`,
expected: []string{`mkdir`, `C:\Windows\foo\bar`},
},
{ // 5 - command with space in quotes
input: `"command here"`,
expected: []string{`command here`},
},
{ // 6 - argument with escaped quotes (two quotes)
input: `cmd ""arg""`,
expected: []string{`cmd`, `"arg"`},
},
{ // 7 - argument with escaped quotes (backslash)
input: `cmd \"arg\"`,
expected: []string{`cmd`, `"arg"`},
},
{ // 8 - two quotes (escaped) inside an inQuote element
input: `cmd "a ""quoted value"`,
expected: []string{`cmd`, `a "quoted value`},
},
// TODO - see how many quotes are dislayed if we use "", """, """""""
{ // 9 - two quotes outside an inQuote element
input: `cmd a ""quoted value`,
expected: []string{`cmd`, `a`, `"quoted`, `value`},
},
{ // 10 - path with space in quotes
input: `mkdir "C:\directory name\foobar"`,
expected: []string{`mkdir`, `C:\directory name\foobar`},
},
{ // 11 - space without quotes
input: `mkdir C:\ space`,
expected: []string{`mkdir`, `C:\`, `space`},
},
{ // 12 - space in quotes
input: `mkdir "C:\ space"`,
expected: []string{`mkdir`, `C:\ space`},
},
{ // 13 - UNC
input: `mkdir \\?\C:\Users`,
expected: []string{`mkdir`, `\\?\C:\Users`},
},
{ // 14 - UNC with space
input: `mkdir "\\?\C:\Program Files"`,
expected: []string{`mkdir`, `\\?\C:\Program Files`},
},
{ // 15 - unclosed quotes - treat as if the path ends with quote
input: `mkdir "c:\Program files`,
expected: []string{`mkdir`, `c:\Program files`},
},
{ // 16 - quotes used inside the argument
input: `mkdir "c:\P"rogra"m f"iles`,
expected: []string{`mkdir`, `c:\Program files`},
},
}
for i, test := range tests {
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
actual := parseWindowsCommand(test.input)
if len(actual) != len(test.expected) {
t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual)
continue
}
for j := 0; j < len(actual); j++ {
if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart {
t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j)
}
}
}
}
func TestSplitCommandAndArgs(t *testing.T) { func TestSplitCommandAndArgs(t *testing.T) {
// force linux parsing. It's more robust and covers error cases
runtimeGoos = "linux"
defer func() {
runtimeGoos = runtime.GOOS
}()
var parseErrorContent = "error parsing command:" var parseErrorContent = "error parsing command:"
var noCommandErrContent = "no command contained in" var noCommandErrContent = "no command contained in"
...@@ -16,84 +181,42 @@ func TestSplitCommandAndArgs(t *testing.T) { ...@@ -16,84 +181,42 @@ func TestSplitCommandAndArgs(t *testing.T) {
expectedArgs []string expectedArgs []string
expectedErrContent string expectedErrContent string
}{ }{
// Test case 0 - emtpy command // 0 - emtpy command
{ {
input: ``, input: ``,
expectedCommand: ``, expectedCommand: ``,
expectedArgs: nil, expectedArgs: nil,
expectedErrContent: noCommandErrContent, expectedErrContent: noCommandErrContent,
}, },
// Test case 1 - command without arguments // 1 - command without arguments
{ {
input: `command`, input: `command`,
expectedCommand: `command`, expectedCommand: `command`,
expectedArgs: nil, expectedArgs: nil,
expectedErrContent: ``, expectedErrContent: ``,
}, },
// Test case 2 - command with single argument // 2 - command with single argument
{ {
input: `command arg1`, input: `command arg1`,
expectedCommand: `command`, expectedCommand: `command`,
expectedArgs: []string{`arg1`}, expectedArgs: []string{`arg1`},
expectedErrContent: ``, expectedErrContent: ``,
}, },
// Test case 3 - command with multiple arguments // 3 - command with multiple arguments
{ {
input: `command arg1 arg2`, input: `command arg1 arg2`,
expectedCommand: `command`, expectedCommand: `command`,
expectedArgs: []string{`arg1`, `arg2`}, expectedArgs: []string{`arg1`, `arg2`},
expectedErrContent: ``, expectedErrContent: ``,
}, },
// Test case 4 - command with single argument with space character - in quotes // 4 - command with unclosed 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`, input: `command "arg1 arg2`,
expectedCommand: "", expectedCommand: "",
expectedArgs: nil, expectedArgs: nil,
expectedErrContent: parseErrorContent, expectedErrContent: parseErrorContent,
}, },
// Test case 11 - command with unclosed quotes // 5 - command with unclosed quotes
{ {
input: `command 'arg1 arg2"`, input: `command 'arg1 arg2"`,
expectedCommand: "", expectedCommand: "",
...@@ -120,19 +243,49 @@ func TestSplitCommandAndArgs(t *testing.T) { ...@@ -120,19 +243,49 @@ func TestSplitCommandAndArgs(t *testing.T) {
// test if command matches // test if command matches
if test.expectedCommand != actualCommand { if test.expectedCommand != actualCommand {
t.Errorf("Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand) t.Errorf(errorPrefix+"Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand)
} }
// test if arguments match // test if arguments match
if len(test.expectedArgs) != len(actualArgs) { if len(test.expectedArgs) != len(actualArgs) {
t.Errorf("Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs) t.Errorf(errorPrefix+"Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs)
} } else {
// test args only if the count matches.
for j, actualArg := range actualArgs { for j, actualArg := range actualArgs {
expectedArg := test.expectedArgs[j] expectedArg := test.expectedArgs[j]
if actualArg != expectedArg { if actualArg != expectedArg {
t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg) t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg)
}
} }
} }
} }
} }
func ExampleSplitCommandAndArgs() {
var commandLine string
var command string
var args []string
// just for the test - change GOOS and reset it at the end of the test
runtimeGoos = "windows"
defer func() {
runtimeGoos = runtime.GOOS
}()
commandLine = `mkdir /P "C:\Program Files"`
command, args, _ = SplitCommandAndArgs(commandLine)
fmt.Printf("Windows: %s: %s [%s]\n", commandLine, command, strings.Join(args, ","))
// set GOOS to linux
runtimeGoos = "linux"
commandLine = `mkdir -p /path/with\ space`
command, args, _ = SplitCommandAndArgs(commandLine)
fmt.Printf("Linux: %s: %s [%s]\n", commandLine, command, strings.Join(args, ","))
// Output:
// Windows: mkdir /P "C:\Program Files": mkdir [/P,C:\Program Files]
// Linux: mkdir -p /path/with\ space: mkdir [-p,/path/with space]
}
...@@ -97,6 +97,10 @@ func (c Context) URI() string { ...@@ -97,6 +97,10 @@ func (c Context) URI() string {
func (c Context) Host() (string, error) { func (c Context) Host() (string, error) {
host, _, err := net.SplitHostPort(c.Req.Host) host, _, err := net.SplitHostPort(c.Req.Host)
if err != nil { if err != nil {
if !strings.Contains(c.Req.Host, ":") {
// common with sites served on the default port 80
return c.Req.Host, nil
}
return "", err return "", err
} }
return host, nil return host, nil
......
package middleware
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestInclude(t *testing.T) {
context := getContextOrFail(t)
inputFilename := "test_file"
absInFilePath := filepath.Join(fmt.Sprintf("%s", context.Root), inputFilename)
defer func() {
err := os.Remove(absInFilePath)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("Failed to clean test file!")
}
}()
tests := []struct {
fileContent string
expectedContent string
shouldErr bool
expectedErrorContent string
}{
// Test 0 - all good
{
fileContent: `str1 {{ .Root }} str2`,
expectedContent: fmt.Sprintf("str1 %s str2", context.Root),
shouldErr: false,
expectedErrorContent: "",
},
// Test 1 - failure on template.Parse
{
fileContent: `str1 {{ .Root } str2`,
expectedContent: "",
shouldErr: true,
expectedErrorContent: `unexpected "}" in operand`,
},
// Test 3 - failure on template.Execute
{
fileContent: `str1 {{ .InvalidField }} str2`,
expectedContent: "",
shouldErr: true,
expectedErrorContent: `InvalidField is not a field of struct type middleware.Context`,
},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
// WriteFile truncates the contentt
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
if err != nil {
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
}
content, err := context.Include(inputFilename)
if err != nil {
if !test.shouldErr {
t.Errorf(testPrefix+"Expected no error, found [%s]", test.expectedErrorContent, err.Error())
}
if !strings.Contains(err.Error(), test.expectedErrorContent) {
t.Errorf(testPrefix+"Expected error content [%s], found [%s]", test.expectedErrorContent, err.Error())
}
}
if err == nil && test.shouldErr {
t.Errorf(testPrefix+"Expected error [%s] but found nil. Input file was: %s", test.expectedErrorContent, inputFilename)
}
if content != test.expectedContent {
t.Errorf(testPrefix+"Expected content [%s] but found [%s]. Input file was: %s", test.expectedContent, content, inputFilename)
}
}
}
func TestIncludeNotExisting(t *testing.T) {
context := getContextOrFail(t)
_, err := context.Include("not_existing")
if err == nil {
t.Errorf("Expected error but found nil!")
}
}
func TestCookie(t *testing.T) {
tests := []struct {
cookie *http.Cookie
cookieName string
expectedValue string
}{
// Test 0 - happy path
{
cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
cookieName: "cookieName",
expectedValue: "cookieValue",
},
// Test 1 - try to get a non-existing cookie
{
cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
cookieName: "notExisting",
expectedValue: "",
},
// Test 2 - partial name match
{
cookie: &http.Cookie{Name: "cookie", Value: "cookieValue"},
cookieName: "cook",
expectedValue: "",
},
// Test 3 - cookie with optional fields
{
cookie: &http.Cookie{Name: "cookie", Value: "cookieValue", Path: "/path", Domain: "https://localhost", Expires: (time.Now().Add(10 * time.Minute)), MaxAge: 120},
cookieName: "cookie",
expectedValue: "cookieValue",
},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
// reinitialize the context for each test
context := getContextOrFail(t)
context.Req.AddCookie(test.cookie)
actualCookieVal := context.Cookie(test.cookieName)
if actualCookieVal != test.expectedValue {
t.Errorf(testPrefix+"Expected cookie value [%s] but found [%s] for cookie with name %s", test.expectedValue, actualCookieVal, test.cookieName)
}
}
}
func TestCookieMultipleCookies(t *testing.T) {
context := getContextOrFail(t)
cookieNameBase, cookieValueBase := "cookieName", "cookieValue"
// make sure that there's no state and multiple requests for different cookies return the correct result
for i := 0; i < 10; i++ {
context.Req.AddCookie(&http.Cookie{Name: fmt.Sprintf("%s%d", cookieNameBase, i), Value: fmt.Sprintf("%s%d", cookieValueBase, i)})
}
for i := 0; i < 10; i++ {
expectedCookieVal := fmt.Sprintf("%s%d", cookieValueBase, i)
actualCookieVal := context.Cookie(fmt.Sprintf("%s%d", cookieNameBase, i))
if actualCookieVal != expectedCookieVal {
t.Fatalf("Expected cookie value %s, found %s", expectedCookieVal, actualCookieVal)
}
}
}
func TestHeader(t *testing.T) {
context := getContextOrFail(t)
headerKey, headerVal := "Header1", "HeaderVal1"
context.Req.Header.Add(headerKey, headerVal)
actualHeaderVal := context.Header(headerKey)
if actualHeaderVal != headerVal {
t.Errorf("Expected header %s, found %s", headerVal, actualHeaderVal)
}
missingHeaderVal := context.Header("not-existing")
if missingHeaderVal != "" {
t.Errorf("Expected empty header value, found %s", missingHeaderVal)
}
}
func TestIP(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
inputRemoteAddr string
expectedIP string
}{
// Test 0 - ipv4 with port
{"1.1.1.1:1111", "1.1.1.1"},
// Test 1 - ipv4 without port
{"1.1.1.1", "1.1.1.1"},
// Test 2 - ipv6 with port
{"[::1]:11", "::1"},
// Test 3 - ipv6 without port and brackets
{"[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]"},
// Test 4 - ipv6 with zone and port
{`[fe80:1::3%eth0]:44`, `fe80:1::3%eth0`},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
context.Req.RemoteAddr = test.inputRemoteAddr
actualIP := context.IP()
if actualIP != test.expectedIP {
t.Errorf(testPrefix+"Expected IP %s, found %s", test.expectedIP, actualIP)
}
}
}
func TestURL(t *testing.T) {
context := getContextOrFail(t)
inputURL := "http://localhost"
context.Req.RequestURI = inputURL
if inputURL != context.URI() {
t.Errorf("Expected url %s, found %s", inputURL, context.URI())
}
}
func TestHost(t *testing.T) {
tests := []struct {
input string
expectedHost string
shouldErr bool
}{
{
input: "localhost:123",
expectedHost: "localhost",
shouldErr: false,
},
{
input: "localhost",
expectedHost: "localhost",
shouldErr: false,
},
{
input: "[::]",
expectedHost: "",
shouldErr: true,
},
}
for _, test := range tests {
testHostOrPort(t, true, test.input, test.expectedHost, test.shouldErr)
}
}
func TestPort(t *testing.T) {
tests := []struct {
input string
expectedPort string
shouldErr bool
}{
{
input: "localhost:123",
expectedPort: "123",
shouldErr: false,
},
{
input: "localhost",
expectedPort: "",
shouldErr: true, // missing port in address
},
{
input: ":8080",
expectedPort: "8080",
shouldErr: false,
},
}
for _, test := range tests {
testHostOrPort(t, false, test.input, test.expectedPort, test.shouldErr)
}
}
func testHostOrPort(t *testing.T, isTestingHost bool, input, expectedResult string, shouldErr bool) {
context := getContextOrFail(t)
context.Req.Host = input
var actualResult, testedObject string
var err error
if isTestingHost {
actualResult, err = context.Host()
testedObject = "host"
} else {
actualResult, err = context.Port()
testedObject = "port"
}
if shouldErr && err == nil {
t.Errorf("Expected error, found nil!")
return
}
if !shouldErr && err != nil {
t.Errorf("Expected no error, found %s", err)
return
}
if actualResult != expectedResult {
t.Errorf("Expected %s %s, found %s", testedObject, expectedResult, actualResult)
}
}
func TestMethod(t *testing.T) {
context := getContextOrFail(t)
method := "POST"
context.Req.Method = method
if method != context.Method() {
t.Errorf("Expected method %s, found %s", method, context.Method())
}
}
func TestPathMatches(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
urlStr string
pattern string
shouldMatch bool
}{
// Test 0
{
urlStr: "http://localhost/",
pattern: "",
shouldMatch: true,
},
// Test 1
{
urlStr: "http://localhost",
pattern: "",
shouldMatch: true,
},
// Test 1
{
urlStr: "http://localhost/",
pattern: "/",
shouldMatch: true,
},
// Test 3
{
urlStr: "http://localhost/?param=val",
pattern: "/",
shouldMatch: true,
},
// Test 4
{
urlStr: "http://localhost/dir1/dir2",
pattern: "/dir2",
shouldMatch: false,
},
// Test 5
{
urlStr: "http://localhost/dir1/dir2",
pattern: "/dir1",
shouldMatch: true,
},
// Test 6
{
urlStr: "http://localhost:444/dir1/dir2",
pattern: "/dir1",
shouldMatch: true,
},
// Test 7
{
urlStr: "http://localhost/dir1/dir2",
pattern: "*/dir2",
shouldMatch: false,
},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
var err error
context.Req.URL, err = url.Parse(test.urlStr)
if err != nil {
t.Fatalf("Failed to prepare test URL from string %s! Error was: %s", test.urlStr, err)
}
matches := context.PathMatches(test.pattern)
if matches != test.shouldMatch {
t.Errorf(testPrefix+"Expected and actual result differ: expected to match [%t], actual matches [%t]", test.shouldMatch, matches)
}
}
}
func TestTruncate(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
inputString string
inputLength int
expected string
}{
// Test 0 - small length
{
inputString: "string",
inputLength: 1,
expected: "s",
},
// Test 1 - exact length
{
inputString: "string",
inputLength: 6,
expected: "string",
},
// Test 2 - bigger length
{
inputString: "string",
inputLength: 10,
expected: "string",
},
}
for i, test := range tests {
actual := context.Truncate(test.inputString, test.inputLength)
if actual != test.expected {
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was Truncate(%q, %d)", test.expected, actual, test.inputString, test.inputLength)
}
}
}
func TestStripHTML(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
input string
expected string
}{
// Test 0 - no tags
{
input: `h1`,
expected: `h1`,
},
// Test 1 - happy path
{
input: `<h1>h1</h1>`,
expected: `h1`,
},
// Test 2 - tag in quotes
{
input: `<h1">">h1</h1>`,
expected: `h1`,
},
// Test 3 - multiple tags
{
input: `<h1><b>h1</b></h1>`,
expected: `h1`,
},
// Test 4 - tags not closed
{
input: `<h1`,
expected: `<h1`,
},
// Test 5 - false start
{
input: `<h1<b>hi`,
expected: `<h1hi`,
},
}
for i, test := range tests {
actual := context.StripHTML(test.input)
if actual != test.expected {
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was StripHTML(%s)", test.expected, actual, test.input)
}
}
}
func TestStripExt(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
input string
expected string
}{
// Test 0 - empty input
{
input: "",
expected: "",
},
// Test 1 - relative file with ext
{
input: "file.ext",
expected: "file",
},
// Test 2 - relative file without ext
{
input: "file",
expected: "file",
},
// Test 3 - absolute file without ext
{
input: "/file",
expected: "/file",
},
// Test 4 - absolute file with ext
{
input: "/file.ext",
expected: "/file",
},
// Test 5 - with ext but ends with /
{
input: "/dir.ext/",
expected: "/dir.ext/",
},
// Test 6 - file with ext under dir with ext
{
input: "/dir.ext/file.ext",
expected: "/dir.ext/file",
},
}
for i, test := range tests {
actual := context.StripExt(test.input)
if actual != test.expected {
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was StripExt(%q)", test.expected, actual, test.input)
}
}
}
func initTestContext() (Context, error) {
body := bytes.NewBufferString("request body")
request, err := http.NewRequest("GET", "https://localhost", body)
if err != nil {
return Context{}, err
}
return Context{Root: http.Dir(os.TempDir()), Req: request}, nil
}
func getContextOrFail(t *testing.T) Context {
context, err := initTestContext()
if err != nil {
t.Fatalf("Failed to prepare test context")
}
return context
}
func getTestPrefix(testN int) string {
return fmt.Sprintf("Test [%d]: ", testN)
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package fastcgi package fastcgi
import ( import (
"errors"
"io" "io"
"net/http" "net/http"
"os" "os"
...@@ -46,10 +47,21 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) ...@@ -46,10 +47,21 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
fpath := r.URL.Path fpath := r.URL.Path
if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok { if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
fpath = idx fpath = idx
// Index file present.
// If request path cannot be split, return error.
if !h.canSplit(fpath, rule) {
return http.StatusInternalServerError, ErrIndexMissingSplit
}
} else {
// No index file present.
// If request path cannot be split, ignore request.
if !h.canSplit(fpath, rule) {
continue
}
} }
// These criteria work well in this order for PHP sites // These criteria work well in this order for PHP sites
if fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) || !h.exists(fpath) { if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
// Create environment for CGI script // Create environment for CGI script
env, err := h.buildEnv(r, rule, fpath) env, err := h.buildEnv(r, rule, fpath)
...@@ -137,6 +149,10 @@ func (h Handler) exists(path string) bool { ...@@ -137,6 +149,10 @@ func (h Handler) exists(path string) bool {
return false return false
} }
func (h Handler) canSplit(path string, rule Rule) bool {
return strings.Contains(path, rule.SplitPath)
}
// buildEnv returns a set of CGI environment variables for the request. // buildEnv returns a set of CGI environment variables for the request.
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) { func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
var env map[string]string var env map[string]string
...@@ -153,22 +169,15 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string] ...@@ -153,22 +169,15 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
ip = r.RemoteAddr ip = r.RemoteAddr
} }
// Split path in preparation for env variables // Split path in preparation for env variables.
// Previous h.canSplit checks ensure this can never be -1.
splitPos := strings.Index(fpath, rule.SplitPath) splitPos := strings.Index(fpath, rule.SplitPath)
var docURI, scriptName, scriptFilename, pathInfo string
if splitPos == -1 { // Request has the extension; path was split successfully
// Request doesn't have the extension, so assume index file in root docURI := fpath[:splitPos+len(rule.SplitPath)]
docURI = "/" + rule.IndexFiles[0] pathInfo := fpath[splitPos+len(rule.SplitPath):]
scriptName = "/" + rule.IndexFiles[0] scriptName := fpath
scriptFilename = filepath.Join(h.AbsRoot, rule.IndexFiles[0]) scriptFilename := absPath
pathInfo = fpath
} else {
// Request has the extension; path was split successfully
docURI = fpath[:splitPos+len(rule.SplitPath)]
pathInfo = fpath[splitPos+len(rule.SplitPath):]
scriptName = fpath
scriptFilename = absPath
}
// Strip PATH_INFO from SCRIPT_NAME // Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, pathInfo) scriptName = strings.TrimSuffix(scriptName, pathInfo)
...@@ -267,4 +276,8 @@ type Rule struct { ...@@ -267,4 +276,8 @@ type Rule struct {
EnvVars [][2]string EnvVars [][2]string
} }
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") var (
headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
ErrIndexMissingSplit = errors.New("configured index file(s) must include split value")
)
...@@ -62,8 +62,8 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st ...@@ -62,8 +62,8 @@ func (fh *fileHandler) serveFile(w http.ResponseWriter, r *http.Request, name st
} }
defer f.Close() defer f.Close()
d, err1 := f.Stat() d, err := f.Stat()
if err1 != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return http.StatusNotFound, nil return http.StatusNotFound, nil
} else if os.IsPermission(err) { } else if os.IsPermission(err) {
......
package middleware
import (
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
var testDir = filepath.Join(os.TempDir(), "caddy_testdir")
var customErr = errors.New("Custom Error")
// testFiles is a map with relative paths to test files as keys and file content as values.
// The map represents the following structure:
// - $TEMP/caddy_testdir/
// '-- file1.html
// '-- dirwithindex/
// '---- index.html
// '-- dir/
// '---- file2.html
// '---- hidden.html
var testFiles = map[string]string{
"file1.html": "<h1>file1.html</h1>",
filepath.Join("dirwithindex", "index.html"): "<h1>dirwithindex/index.html</h1>",
filepath.Join("dir", "file2.html"): "<h1>dir/file2.html</h1>",
filepath.Join("dir", "hidden.html"): "<h1>dir/hidden.html</h1>",
}
// TestServeHTTP covers positive scenarios when serving files.
func TestServeHTTP(t *testing.T) {
beforeServeHttpTest(t)
defer afterServeHttpTest(t)
fileserver := FileServer(http.Dir(testDir), []string{"hidden.html"})
movedPermanently := "Moved Permanently"
tests := []struct {
url string
expectedStatus int
expectedBodyContent string
}{
// Test 0 - access withoutt any path
{
url: "https://foo",
expectedStatus: http.StatusNotFound,
},
// Test 1 - access root (without index.html)
{
url: "https://foo/",
expectedStatus: http.StatusNotFound,
},
// Test 2 - access existing file
{
url: "https://foo/file1.html",
expectedStatus: http.StatusOK,
expectedBodyContent: testFiles["file1.html"],
},
// Test 3 - access folder with index file with trailing slash
{
url: "https://foo/dirwithindex/",
expectedStatus: http.StatusOK,
expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")],
},
// Test 4 - access folder with index file without trailing slash
{
url: "https://foo/dirwithindex",
expectedStatus: http.StatusMovedPermanently,
expectedBodyContent: movedPermanently,
},
// Test 5 - access folder without index file
{
url: "https://foo/dir/",
expectedStatus: http.StatusNotFound,
},
// Test 6 - access folder withtout trailing slash
{
url: "https://foo/dir",
expectedStatus: http.StatusMovedPermanently,
expectedBodyContent: movedPermanently,
},
// Test 6 - access file with trailing slash
{
url: "https://foo/file1.html/",
expectedStatus: http.StatusMovedPermanently,
expectedBodyContent: movedPermanently,
},
// Test 7 - access not existing path
{
url: "https://foo/not_existing",
expectedStatus: http.StatusNotFound,
},
// Test 8 - access a file, marked as hidden
{
url: "https://foo/dir/hidden.html",
expectedStatus: http.StatusNotFound,
},
// Test 9 - access a index file directly
{
url: "https://foo/dirwithindex/index.html",
expectedStatus: http.StatusOK,
expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")],
},
// Test 10 - send a request with query params
{
url: "https://foo/dir?param1=val",
expectedStatus: http.StatusMovedPermanently,
expectedBodyContent: movedPermanently,
},
}
for i, test := range tests {
responseRecorder := httptest.NewRecorder()
request, err := http.NewRequest("GET", test.url, strings.NewReader(""))
status, err := fileserver.ServeHTTP(responseRecorder, request)
// check if error matches expectations
if err != nil {
t.Errorf(getTestPrefix(i)+"Serving file at %s failed. Error was: %v", test.url, err)
}
// check status code
if test.expectedStatus != status {
t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status)
}
// check body content
if !strings.Contains(responseRecorder.Body.String(), test.expectedBodyContent) {
t.Errorf(getTestPrefix(i)+"Expected body to contain %q, found %q", test.expectedBodyContent, responseRecorder.Body.String())
}
}
}
// beforeServeHttpTest creates a test directory with the structure, defined in the variable testFiles
func beforeServeHttpTest(t *testing.T) {
// make the root test dir
err := os.Mkdir(testDir, os.ModePerm)
if err != nil {
if !os.IsExist(err) {
t.Fatalf("Failed to create test dir. Error was: %v", err)
return
}
}
for relFile, fileContent := range testFiles {
absFile := filepath.Join(testDir, relFile)
// make sure the parent directories exist
parentDir := filepath.Dir(absFile)
_, err = os.Stat(parentDir)
if err != nil {
os.MkdirAll(parentDir, os.ModePerm)
}
// now create the test files
f, err := os.Create(absFile)
if err != nil {
t.Fatalf("Failed to create test file %s. Error was: %v", absFile, err)
return
}
// and fill them with content
_, err = f.WriteString(fileContent)
if err != nil {
t.Fatalf("Failed to write to %s. Error was: %v", absFile, err)
return
}
f.Close()
}
}
// afterServeHttpTest removes the test dir and all its content
func afterServeHttpTest(t *testing.T) {
// cleans up everything under the test dir. No need to clean the individual files.
err := os.RemoveAll(testDir)
if err != nil {
t.Fatalf("Failed to clean up test dir %s. Error was: %v", testDir, err)
}
}
// failingFS implements the http.FileSystem interface. The Open method always returns the error, assigned to err
type failingFS struct {
err error // the error to return when Open is called
fileImpl http.File // inject the file implementation
}
// Open returns the assigned failingFile and error
func (f failingFS) Open(path string) (http.File, error) {
return f.fileImpl, f.err
}
// failingFile implements http.File but returns a predefined error on every Stat() method call.
type failingFile struct {
http.File
err error
}
// Stat returns nil FileInfo and the provided error on every call
func (ff failingFile) Stat() (os.FileInfo, error) {
return nil, ff.err
}
// Close is noop and returns no error
func (ff failingFile) Close() error {
return nil
}
// TestServeHTTPFailingFS tests error cases where the Open function fails with various errors.
func TestServeHTTPFailingFS(t *testing.T) {
tests := []struct {
fsErr error
expectedStatus int
expectedErr error
expectedHeaders map[string]string
}{
{
fsErr: os.ErrNotExist,
expectedStatus: http.StatusNotFound,
expectedErr: nil,
},
{
fsErr: os.ErrPermission,
expectedStatus: http.StatusForbidden,
expectedErr: os.ErrPermission,
},
{
fsErr: customErr,
expectedStatus: http.StatusServiceUnavailable,
expectedErr: customErr,
expectedHeaders: map[string]string{"Retry-After": "5"},
},
}
for i, test := range tests {
// initialize a file server with the failing FileSystem
fileserver := FileServer(failingFS{err: test.fsErr}, nil)
// prepare the request and response
request, err := http.NewRequest("GET", "https://foo/", nil)
if err != nil {
t.Fatalf("Failed to build request. Error was: %v", err)
}
responseRecorder := httptest.NewRecorder()
status, actualErr := fileserver.ServeHTTP(responseRecorder, request)
// check the status
if status != test.expectedStatus {
t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status)
}
// check the error
if actualErr != test.expectedErr {
t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr)
}
// check the headers - a special case for server under load
if test.expectedHeaders != nil && len(test.expectedHeaders) > 0 {
for expectedKey, expectedVal := range test.expectedHeaders {
actualVal := responseRecorder.Header().Get(expectedKey)
if expectedVal != actualVal {
t.Errorf(getTestPrefix(i)+"Expected header %s: %s, found %s", expectedKey, expectedVal, actualVal)
}
}
}
}
}
// TestServeHTTPFailingStat tests error cases where the initial Open function succeeds, but the Stat method on the opened file fails.
func TestServeHTTPFailingStat(t *testing.T) {
tests := []struct {
statErr error
expectedStatus int
expectedErr error
}{
{
statErr: os.ErrNotExist,
expectedStatus: http.StatusNotFound,
expectedErr: nil,
},
{
statErr: os.ErrPermission,
expectedStatus: http.StatusForbidden,
expectedErr: os.ErrPermission,
},
{
statErr: customErr,
expectedStatus: http.StatusInternalServerError,
expectedErr: customErr,
},
}
for i, test := range tests {
// initialize a file server. The FileSystem will not fail, but calls to the Stat method of the returned File object will
fileserver := FileServer(failingFS{err: nil, fileImpl: failingFile{err: test.statErr}}, nil)
// prepare the request and response
request, err := http.NewRequest("GET", "https://foo/", nil)
if err != nil {
t.Fatalf("Failed to build request. Error was: %v", err)
}
responseRecorder := httptest.NewRecorder()
status, actualErr := fileserver.ServeHTTP(responseRecorder, request)
// check the status
if status != test.expectedStatus {
t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status)
}
// check the error
if actualErr != test.expectedErr {
t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr)
}
}
}
...@@ -17,18 +17,18 @@ import ( ...@@ -17,18 +17,18 @@ import (
// It only generates static files if it is enabled (cfg.StaticDir // It only generates static files if it is enabled (cfg.StaticDir
// must be set). // must be set).
func GenerateStatic(md Markdown, cfg *Config) error { func GenerateStatic(md Markdown, cfg *Config) error {
generated, err := generateLinks(md, cfg)
if err != nil {
return err
}
// No new file changes, return.
if !generated {
return nil
}
// If static site generation is enabled. // If static site generation is enabled.
if cfg.StaticDir != "" { if cfg.StaticDir != "" {
generated, err := generateLinks(md, cfg)
if err != nil {
return err
}
// No new file changes, return.
if !generated {
return nil
}
if err := generateStaticHTML(md, cfg); err != nil { if err := generateStaticHTML(md, cfg); err != nil {
return err return err
} }
......
...@@ -136,6 +136,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -136,6 +136,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// generation, serve the static page // generation, serve the static page
if fs.ModTime().Before(fs1.ModTime()) { if fs.ModTime().Before(fs1.ModTime()) {
if html, err := ioutil.ReadFile(filepath); err == nil { if html, err := ioutil.ReadFile(filepath); err == nil {
middleware.SetLastModifiedHeader(w, fs1.ModTime())
w.Write(html) w.Write(html)
return http.StatusOK, nil return http.StatusOK, nil
} }
...@@ -162,6 +163,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -162,6 +163,7 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
middleware.SetLastModifiedHeader(w, fs.ModTime())
w.Write(html) w.Write(html)
return http.StatusOK, nil return http.StatusOK, nil
} }
......
...@@ -92,7 +92,7 @@ func TestMarkdown(t *testing.T) { ...@@ -92,7 +92,7 @@ func TestMarkdown(t *testing.T) {
expectedBody := `<!DOCTYPE html> expectedBody := `<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Markdown test</title> <title>Markdown test 1</title>
</head> </head>
<body> <body>
<h1>Header</h1> <h1>Header</h1>
...@@ -102,11 +102,10 @@ Welcome to A Caddy website! ...@@ -102,11 +102,10 @@ Welcome to A Caddy website!
<p>Body</p> <p>Body</p>
<p><code>go <pre><code class="language-go">func getTrue() bool {
func getTrue() bool {
return true return true
} }
</code></p> </code></pre>
</body> </body>
</html> </html>
...@@ -129,7 +128,7 @@ func getTrue() bool { ...@@ -129,7 +128,7 @@ func getTrue() bool {
expectedBody = `<!DOCTYPE html> expectedBody = `<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Markdown test</title> <title>Markdown test 2</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="/resources/css/log.css"> <link rel="stylesheet" href="/resources/css/log.css">
<link rel="stylesheet" href="/resources/css/default.css"> <link rel="stylesheet" href="/resources/css/default.css">
...@@ -143,11 +142,10 @@ func getTrue() bool { ...@@ -143,11 +142,10 @@ func getTrue() bool {
<p>Body</p> <p>Body</p>
<p><code>go <pre><code class="language-go">func getTrue() bool {
func getTrue() bool {
return true return true
} }
</code></p> </code></pre>
</body> </body>
</html>` </html>`
......
...@@ -65,7 +65,8 @@ func (md Markdown) Process(c *Config, requestPath string, b []byte, ctx middlewa ...@@ -65,7 +65,8 @@ func (md Markdown) Process(c *Config, requestPath string, b []byte, ctx middlewa
} }
// process markdown // process markdown
markdown = blackfriday.Markdown(markdown, c.Renderer, 0) extns := blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_STRIKETHROUGH
markdown = blackfriday.Markdown(markdown, c.Renderer, extns)
// set it as body for template // set it as body for template
metadata.Variables["body"] = string(markdown) metadata.Variables["body"] = string(markdown)
......
--- ---
title: Markdown test title: Markdown test 1
sitename: A Caddy website sitename: A Caddy website
--- ---
......
--- ---
title: Markdown test title: Markdown test 2
sitename: A Caddy website sitename: A Caddy website
--- ---
......
...@@ -4,6 +4,7 @@ package middleware ...@@ -4,6 +4,7 @@ package middleware
import ( import (
"net/http" "net/http"
"path" "path"
"time"
) )
type ( type (
...@@ -78,3 +79,30 @@ func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, ...@@ -78,3 +79,30 @@ func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string,
} }
return "", false return "", false
} }
// SetLastModifiedHeader checks if the provided modTime is valid and if it is sets it
// as a Last-Modified header to the ResponseWriter. If the modTime is in the future
// the current time is used instead.
func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
if modTime.IsZero() || modTime.Equal(time.Unix(0, 0)) {
// the time does not appear to be valid. Don't put it in the response
return
}
// RFC 2616 - Section 14.29 - Last-Modified:
// An origin server MUST NOT send a Last-Modified date which is later than the
// server's time of message origination. In such cases, where the resource's last
// modification would indicate some time in the future, the server MUST replace
// that date with the message origination date.
now := currentTime()
if modTime.After(now) {
modTime = now
}
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
}
// currentTime returns time.Now() everytime it's called. It's used for mocking in tests.
var currentTime = func() time.Time {
return time.Now()
}
package middleware package middleware
import ( import (
"fmt"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"time"
) )
func TestIndexfile(t *testing.T) { func TestIndexfile(t *testing.T) {
...@@ -42,3 +45,64 @@ func TestIndexfile(t *testing.T) { ...@@ -42,3 +45,64 @@ func TestIndexfile(t *testing.T) {
} }
} }
} }
func TestSetLastModified(t *testing.T) {
nowTime := time.Now()
// ovewrite the function to return reliable time
originalGetCurrentTimeFunc := currentTime
currentTime = func() time.Time {
return nowTime
}
defer func() {
currentTime = originalGetCurrentTimeFunc
}()
pastTime := nowTime.Truncate(1 * time.Hour)
futureTime := nowTime.Add(1 * time.Hour)
tests := []struct {
inputModTime time.Time
expectedIsHeaderSet bool
expectedLastModified string
}{
{
inputModTime: pastTime,
expectedIsHeaderSet: true,
expectedLastModified: pastTime.UTC().Format(http.TimeFormat),
},
{
inputModTime: nowTime,
expectedIsHeaderSet: true,
expectedLastModified: nowTime.UTC().Format(http.TimeFormat),
},
{
inputModTime: futureTime,
expectedIsHeaderSet: true,
expectedLastModified: nowTime.UTC().Format(http.TimeFormat),
},
{
inputModTime: time.Time{},
expectedIsHeaderSet: false,
},
}
for i, test := range tests {
responseRecorder := httptest.NewRecorder()
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
SetLastModifiedHeader(responseRecorder, test.inputModTime)
actualLastModifiedHeader := responseRecorder.Header().Get("Last-Modified")
if test.expectedIsHeaderSet && actualLastModifiedHeader == "" {
t.Fatalf(errorPrefix + "Expected to find Last-Modified header, but found nothing")
}
if !test.expectedIsHeaderSet && actualLastModifiedHeader != "" {
t.Fatalf(errorPrefix+"Did not expect to find Last-Modified header, but found one [%s].", actualLastModifiedHeader)
}
if test.expectedLastModified != actualLastModifiedHeader {
t.Errorf(errorPrefix+"Expected Last-Modified content [%s], found [%s}", test.expectedLastModified, actualLastModifiedHeader)
}
}
}
...@@ -33,8 +33,18 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -33,8 +33,18 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// Create execution context // Create execution context
ctx := middleware.Context{Root: t.FileSys, Req: r, URL: r.URL} ctx := middleware.Context{Root: t.FileSys, Req: r, URL: r.URL}
// New template
templateName := filepath.Base(fpath)
tpl := template.New(templateName)
// Set delims
if rule.Delims != [2]string{} {
tpl.Delims(rule.Delims[0], rule.Delims[1])
}
// Build the template // Build the template
tpl, err := template.ParseFiles(filepath.Join(t.Root, fpath)) templatePath := filepath.Join(t.Root, fpath)
tpl, err := tpl.ParseFiles(templatePath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return http.StatusNotFound, nil return http.StatusNotFound, nil
...@@ -50,6 +60,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error ...@@ -50,6 +60,12 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
templateInfo, err := os.Stat(templatePath)
if err == nil {
// add the Last-Modified header if we were able to optain the information
middleware.SetLastModifiedHeader(w, templateInfo.ModTime())
}
buf.WriteTo(w) buf.WriteTo(w)
return http.StatusOK, nil return http.StatusOK, nil
...@@ -75,4 +91,5 @@ type Rule struct { ...@@ -75,4 +91,5 @@ type Rule struct {
Path string Path string
Extensions []string Extensions []string
IndexFiles []string IndexFiles []string
Delims [2]string
} }
...@@ -23,6 +23,7 @@ func Test(t *testing.T) { ...@@ -23,6 +23,7 @@ func Test(t *testing.T) {
Extensions: []string{".html", ".htm"}, Extensions: []string{".html", ".htm"},
IndexFiles: []string{"index.html", "index.htm"}, IndexFiles: []string{"index.html", "index.htm"},
Path: "/images", Path: "/images",
Delims: [2]string{"{%", "%}"},
}, },
}, },
Root: "./testdata", Root: "./testdata",
...@@ -94,6 +95,30 @@ func Test(t *testing.T) { ...@@ -94,6 +95,30 @@ func Test(t *testing.T) {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody) t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
} }
/*
* Test tmpl on /images/img2.htm
*/
req, err = http.NewRequest("GET", "/images/img2.htm", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
rec = httptest.NewRecorder()
tmpl.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody = rec.Body.String()
expectedBody = `<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html>
`
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
/* /*
* Test tmplroot on /root.html * Test tmplroot on /root.html
*/ */
......
<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html> <!DOCTYPE html><html><head><title>img</title></head><body>{%.Include "header.html"%}</body></html>
<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html>
...@@ -172,7 +172,7 @@ func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) { ...@@ -172,7 +172,7 @@ func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) {
conn.SetReadDeadline(time.Now().Add(pongWait)) conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
tickerChan := make(chan bool) tickerChan := make(chan bool)
defer func() { tickerChan <- true }() // make sure to close the ticker when we are done. defer close(tickerChan) // make sure to close the ticker when we are done.
go ticker(conn, tickerChan) go ticker(conn, tickerChan)
for { for {
...@@ -213,10 +213,7 @@ func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) { ...@@ -213,10 +213,7 @@ func reader(conn *websocket.Conn, stdout io.ReadCloser, stdin io.WriteCloser) {
// between the server and client to keep it alive with ping messages. // between the server and client to keep it alive with ping messages.
func ticker(conn *websocket.Conn, c chan bool) { func ticker(conn *websocket.Conn, c chan bool) {
ticker := time.NewTicker(pingPeriod) ticker := time.NewTicker(pingPeriod)
defer func() { defer ticker.Stop()
ticker.Stop()
close(c)
}()
for { // blocking loop with select to wait for stimulation. for { // blocking loop with select to wait for stimulation.
select { select {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment