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
}
This diff is collapsed.
...@@ -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
......
This diff is collapsed.
...@@ -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