├── caddyhttp ├── rewrite │ ├── testdata │ │ ├── testdir │ │ │ └── empty │ │ └── testfile │ ├── to_test.go │ ├── to.go │ └── setup.go ├── browse │ ├── testdata │ │ ├── header.html │ │ ├── photos │ │ │ ├── hidden.html │ │ │ ├── test3.html │ │ │ ├── test.html │ │ │ ├── test2.html │ │ │ └── test1 │ │ │ │ └── test.html │ │ └── photos.tpl │ └── setup_test.go ├── templates │ ├── testdata │ │ ├── header.html │ │ ├── images │ │ │ ├── header.html │ │ │ ├── img.htm │ │ │ └── img2.htm │ │ ├── root.html │ │ └── photos │ │ │ └── test.html │ ├── setup.go │ └── templates.go ├── markdown │ ├── summary │ │ ├── summary_test.go │ │ └── summary.go │ ├── metadata │ │ ├── metadata_yaml.go │ │ ├── metadata_none.go │ │ ├── metadata_toml.go │ │ ├── metadata_json.go │ │ └── metadata.go │ ├── process_test.go │ ├── template.go │ └── process.go ├── websocket │ ├── websocket_test.go │ ├── setup.go │ └── setup_test.go ├── bind │ ├── bind.go │ └── bind_test.go ├── push │ ├── push.go │ └── handler.go ├── caddyhttp_test.go ├── index │ ├── index.go │ └── index_test.go ├── pprof │ ├── setup_test.go │ ├── setup.go │ ├── pprof_test.go │ └── pprof.go ├── httpserver │ ├── path.go │ ├── error.go │ ├── recorder_test.go │ ├── middleware_test.go │ ├── pathcleaner.go │ └── roller.go ├── proxy │ ├── setup.go │ ├── body.go │ └── body_test.go ├── mime │ ├── mime.go │ ├── mime_test.go │ ├── setup_test.go │ └── setup.go ├── internalsrv │ ├── setup.go │ ├── setup_test.go │ └── internal.go ├── expvar │ ├── expvar_test.go │ ├── setup_test.go │ ├── expvar.go │ └── setup.go ├── extensions │ ├── setup.go │ ├── ext.go │ └── setup_test.go ├── caddyhttp.go ├── root │ └── root.go ├── status │ ├── status.go │ ├── setup.go │ ├── status_test.go │ └── setup_test.go ├── redirect │ └── redirect.go ├── header │ ├── setup.go │ ├── header_test.go │ └── setup_test.go ├── fastcgi │ ├── fcgi_test.php │ ├── dialer.go │ └── dialer_test.go ├── basicauth │ └── setup.go ├── log │ ├── log.go │ └── setup.go ├── gzip │ ├── requestfilter.go │ ├── requestfilter_test.go │ ├── responsefilter.go │ ├── responsefilter_test.go │ ├── setup_test.go │ ├── gzip_test.go │ └── setup.go ├── timeouts │ └── timeouts.go └── errors │ └── setup.go ├── caddyfile ├── testdata │ ├── import_test1.txt │ ├── import_test2.txt │ ├── import_glob2.txt │ ├── import_glob1.txt │ └── import_glob0.txt └── lexer.go ├── caddytls ├── client_test.go ├── storagetest │ ├── memorystorage_test.go │ └── storagetest_test.go ├── filestorage_test.go ├── httphandler.go ├── httphandler_test.go ├── handshake_test.go └── certificates_test.go ├── sigtrap_windows.go ├── dist ├── gitcookie.sh.enc ├── automate_test.go ├── init │ ├── linux-sysvinit │ │ ├── README.md │ │ └── caddy │ ├── mac-launchd │ │ ├── README.md │ │ └── com.caddyserver.web.plist │ ├── linux-upstart │ │ ├── caddy.conf │ │ ├── README.md │ │ ├── caddy.conf.ubuntu-12.04 │ │ └── caddy.conf.centos-6 │ ├── README.md │ ├── linux-systemd │ │ └── caddy.service │ └── freebsd │ │ └── caddy └── README.txt ├── rlimit_windows.go ├── .gitignore ├── caddy ├── main_test.go ├── main.go ├── caddymain │ └── run_test.go └── build.bash ├── assets_test.go ├── rlimit_posix.go ├── appveyor.yml ├── .gitattributes ├── assets.go ├── .travis.yml ├── ISSUE_TEMPLATE ├── startupshutdown ├── startupshutdown.go └── startupshutdown_test.go ├── caddy_test.go ├── sigtrap_posix.go ├── sigtrap.go └── commands.go /caddyhttp/rewrite/testdata/testdir/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /caddyhttp/rewrite/testdata/testfile: -------------------------------------------------------------------------------- 1 | empty -------------------------------------------------------------------------------- /caddyfile/testdata/import_test1.txt: -------------------------------------------------------------------------------- 1 | dir2 arg1 arg2 2 | dir3 -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/header.html: -------------------------------------------------------------------------------- 1 |

Header

2 | -------------------------------------------------------------------------------- /caddytls/client_test.go: -------------------------------------------------------------------------------- 1 | package caddytls 2 | 3 | // TODO 4 | -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/photos/hidden.html: -------------------------------------------------------------------------------- 1 | Should be hidden 2 | -------------------------------------------------------------------------------- /caddyhttp/templates/testdata/header.html: -------------------------------------------------------------------------------- 1 |

Header title

2 | -------------------------------------------------------------------------------- /caddyfile/testdata/import_test2.txt: -------------------------------------------------------------------------------- 1 | host1 { 2 | dir1 3 | dir2 arg1 4 | } -------------------------------------------------------------------------------- /caddyhttp/templates/testdata/images/header.html: -------------------------------------------------------------------------------- 1 |

Header title

2 | -------------------------------------------------------------------------------- /sigtrap_windows.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | func trapSignalsPosix() {} 4 | -------------------------------------------------------------------------------- /caddyfile/testdata/import_glob2.txt: -------------------------------------------------------------------------------- 1 | glob2.host0 { 2 | dir2 arg1 3 | } 4 | -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/photos/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /caddyfile/testdata/import_glob1.txt: -------------------------------------------------------------------------------- 1 | glob1.host0 { 2 | dir1 3 | dir2 arg1 4 | } 5 | -------------------------------------------------------------------------------- /dist/gitcookie.sh.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex/caddy/master/dist/gitcookie.sh.enc -------------------------------------------------------------------------------- /caddyfile/testdata/import_glob0.txt: -------------------------------------------------------------------------------- 1 | glob0.host0 { 2 | dir2 arg1 3 | } 4 | 5 | glob0.host1 { 6 | } 7 | -------------------------------------------------------------------------------- /caddyhttp/templates/testdata/root.html: -------------------------------------------------------------------------------- 1 | root{{.Include "header.html"}} 2 | -------------------------------------------------------------------------------- /caddyhttp/templates/testdata/images/img.htm: -------------------------------------------------------------------------------- 1 | img{%.Include "header.html"%} 2 | -------------------------------------------------------------------------------- /caddyhttp/templates/testdata/images/img2.htm: -------------------------------------------------------------------------------- 1 | img{{.Include "header.html"}} 2 | -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/photos/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/photos/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 2 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /caddyhttp/templates/testdata/photos/test.html: -------------------------------------------------------------------------------- 1 | test page{{.Include "../header.html"}} 2 | -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/photos/test1/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /rlimit_windows.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | // checkFdlimit issues a warning if the OS limit for 4 | // max file descriptors is below a recommended minimum. 5 | func checkFdlimit() { 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | _gitignore/ 4 | Vagrantfile 5 | .vagrant/ 6 | /.idea 7 | 8 | dist/builds/ 9 | dist/release/ 10 | 11 | error.log 12 | access.log 13 | 14 | /*.conf 15 | Caddyfile 16 | 17 | og_static/ 18 | 19 | .vscode/ -------------------------------------------------------------------------------- /caddyhttp/browse/testdata/photos.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Template 5 | 6 | 7 | {{.Include "header.html"}} 8 |

{{.Path}}

9 | {{range .Items}} 10 | {{.Name}}
11 | {{end}} 12 | 13 | 14 | -------------------------------------------------------------------------------- /caddytls/storagetest/memorystorage_test.go: -------------------------------------------------------------------------------- 1 | package storagetest 2 | 3 | import "testing" 4 | 5 | func TestMemoryStorage(t *testing.T) { 6 | storage := NewInMemoryStorage() 7 | storageTest := &StorageTest{ 8 | Storage: storage, 9 | PostTest: storage.Clear, 10 | } 11 | storageTest.Test(t, false) 12 | } 13 | -------------------------------------------------------------------------------- /caddyhttp/markdown/summary/summary_test.go: -------------------------------------------------------------------------------- 1 | package summary 2 | 3 | import "testing" 4 | 5 | func TestMarkdown(t *testing.T) { 6 | input := []byte(`Testing with just a few words.`) 7 | got := string(Markdown(input, 3)) 8 | if want := "Testing with just"; want != got { 9 | t.Errorf("Expected '%s' but got '%s'", want, got) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /caddytls/filestorage_test.go: -------------------------------------------------------------------------------- 1 | package caddytls 2 | 3 | // *********************************** NOTE ******************************** 4 | // Due to circular package dependencies with the storagetest sub package and 5 | // the fact that we want to use that harness to test file storage, the tests 6 | // for file storage are done in the storagetest package. 7 | -------------------------------------------------------------------------------- /dist/automate_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func TestNumProcs(t *testing.T) { 9 | num := runtime.NumCPU() 10 | n := numProcs() 11 | if n > num || n < 1 { 12 | t.Errorf("Expected numProcs() to return max(NumCPU-1, 1) or at least some "+ 13 | "reasonable value (depending on CI environment), but got n=%d (NumCPU=%d)", n, num) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /caddy/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | // This works because it does not have the same signature as the 6 | // conventional "TestMain" function described in the testing package 7 | // godoc. 8 | func TestMain(t *testing.T) { 9 | var ran bool 10 | run = func() { 11 | ran = true 12 | } 13 | main() 14 | if !ran { 15 | t.Error("Expected Run() to be called, but it wasn't") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /caddy/main.go: -------------------------------------------------------------------------------- 1 | // By moving the application's package main logic into 2 | // a package other than main, it becomes much easier to 3 | // wrap caddy for custom builds that are go-gettable. 4 | // https://forum.caddyserver.com/t/my-wish-for-0-9-go-gettable-custom-builds/59?u=matt 5 | 6 | package main 7 | 8 | import "github.com/mholt/caddy/caddy/caddymain" 9 | 10 | var run = caddymain.Run // replaced for tests 11 | 12 | func main() { 13 | run() 14 | } 15 | -------------------------------------------------------------------------------- /assets_test.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestAssetsPath(t *testing.T) { 10 | if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") { 11 | t.Errorf("Expected path to be a .caddy folder, got: %v", actual) 12 | } 13 | 14 | os.Setenv("CADDYPATH", "testpath") 15 | if actual, expected := AssetsPath(), "testpath"; actual != expected { 16 | t.Errorf("Expected path to be %v, got: %v", expected, actual) 17 | } 18 | os.Setenv("CADDYPATH", "") 19 | } 20 | -------------------------------------------------------------------------------- /dist/init/linux-sysvinit/README.md: -------------------------------------------------------------------------------- 1 | SysVinit conf for Caddy 2 | ======================= 3 | 4 | Usage 5 | ----- 6 | 7 | * Download the appropriate Caddy binary in `/usr/local/bin/caddy` or use `curl https://getcaddy.com | bash`. 8 | * Save the SysVinit config file in `/etc/init.d/caddy`. 9 | * Ensure that the folder `/etc/caddy` exists and that the folder `/etc/ssl/caddy` is owned by `www-data`. 10 | * Create a Caddyfile in `/etc/caddy/Caddyfile` 11 | * Now you can use `service caddy start|stop|restart|reload|status` as `root`. 12 | -------------------------------------------------------------------------------- /caddyhttp/markdown/summary/summary.go: -------------------------------------------------------------------------------- 1 | package summary 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/russross/blackfriday" 7 | ) 8 | 9 | // Markdown formats input using a plain-text renderer, and 10 | // then returns up to the first `wordcount` words as a summary. 11 | func Markdown(input []byte, wordcount int) []byte { 12 | words := bytes.Fields(blackfriday.Markdown(input, renderer{}, 0)) 13 | if wordcount > len(words) { 14 | wordcount = len(words) 15 | } 16 | return bytes.Join(words[0:wordcount], []byte{' '}) 17 | } 18 | -------------------------------------------------------------------------------- /caddyhttp/websocket/websocket_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestBuildEnv(t *testing.T) { 9 | req, err := http.NewRequest("GET", "http://localhost", nil) 10 | if err != nil { 11 | t.Fatal("Error setting up request:", err) 12 | } 13 | req.RemoteAddr = "localhost:50302" 14 | 15 | env, err := buildEnv("/bin/command", req) 16 | if err != nil { 17 | t.Fatal("Didn't expect an error:", err) 18 | } 19 | if len(env) == 0 { 20 | t.Fatalf("Expected non-empty environment; got %#v", env) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /caddyhttp/bind/bind.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("bind", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setupBind, 12 | }) 13 | } 14 | 15 | func setupBind(c *caddy.Controller) error { 16 | config := httpserver.GetConfig(c) 17 | for c.Next() { 18 | if !c.Args(&config.ListenHost) { 19 | return c.ArgErr() 20 | } 21 | config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /dist/init/mac-launchd/README.md: -------------------------------------------------------------------------------- 1 | launchd service for macOS 2 | ========================= 3 | 4 | This is a sample file for a *launchd* service on Mac. 5 | Edit the paths and email in the plist file to match your info. 6 | 7 | Start and Stop the Caddy launchd service using the following commands: 8 | 9 | $ launchctl load ~/Library/LaunchAgents/com.caddyserver.web.plist 10 | $ launchctl unload ~/Library/LaunchAgents/com.caddyserver.web.plist 11 | 12 | More information can be found in this blogpost: [Running Caddy as a service on macOS X server](https://denbeke.be/blog/software/running-caddy-as-a-service-on-macos-os-x-server/) -------------------------------------------------------------------------------- /caddyhttp/push/push.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mholt/caddy/caddyhttp/httpserver" 7 | ) 8 | 9 | type ( 10 | // Rule describes conditions on which resources will be pushed 11 | Rule struct { 12 | Path string 13 | Resources []Resource 14 | } 15 | 16 | // Resource describes resource to be pushed 17 | Resource struct { 18 | Path string 19 | Method string 20 | Header http.Header 21 | } 22 | 23 | // Middleware supports pushing resources to clients 24 | Middleware struct { 25 | Next httpserver.Handler 26 | Rules []Rule 27 | } 28 | 29 | ruleOp func([]Resource) 30 | ) 31 | -------------------------------------------------------------------------------- /dist/init/linux-upstart/caddy.conf: -------------------------------------------------------------------------------- 1 | description "Caddy HTTP/2 web server" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [016] 5 | 6 | console log 7 | 8 | setuid www-data 9 | setgid www-data 10 | 11 | respawn 12 | respawn limit 10 5 13 | 14 | reload signal SIGUSR1 15 | 16 | # Let's Encrypt certificates will be written to this directory. 17 | env CADDYPATH=/etc/ssl/caddy 18 | 19 | limit nofile 1048576 1048576 20 | 21 | script 22 | cd /etc/ssl/caddy 23 | rootdir="$(mktemp -d -t "caddy-run.XXXXXX")" 24 | exec /usr/local/bin/caddy -agree -log=stdout -conf=/etc/caddy/Caddyfile -root=$rootdir 25 | end script 26 | -------------------------------------------------------------------------------- /caddyhttp/caddyhttp_test.go: -------------------------------------------------------------------------------- 1 | package caddyhttp 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/mholt/caddy" 8 | ) 9 | 10 | // TODO: this test could be improved; the purpose is to 11 | // ensure that the standard plugins are in fact plugged in 12 | // and registered properly; this is a quick/naive way to do it. 13 | func TestStandardPlugins(t *testing.T) { 14 | numStandardPlugins := 31 // importing caddyhttp plugs in this many plugins 15 | s := caddy.DescribePlugins() 16 | if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want { 17 | t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rlimit_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package caddy 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | // checkFdlimit issues a warning if the OS limit for 11 | // max file descriptors is below a recommended minimum. 12 | func checkFdlimit() { 13 | const min = 8192 14 | 15 | // Warn if ulimit is too low for production sites 16 | rlimit := &syscall.Rlimit{} 17 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimit) 18 | if err == nil && rlimit.Cur < min { 19 | fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+ 20 | "At least %d is recommended. Fix with \"ulimit -n %d\".\n", rlimit.Cur, min, min) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /caddyhttp/index/index.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/staticfiles" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("index", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setupIndex, 12 | }) 13 | } 14 | 15 | func setupIndex(c *caddy.Controller) error { 16 | var index []string 17 | 18 | for c.Next() { 19 | args := c.RemainingArgs() 20 | 21 | if len(args) == 0 { 22 | return c.Errf("Expected at least one index") 23 | } 24 | 25 | for _, in := range args { 26 | index = append(index, in) 27 | } 28 | 29 | staticfiles.IndexPages = index 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /dist/init/linux-upstart/README.md: -------------------------------------------------------------------------------- 1 | Upstart conf for Caddy 2 | ===================== 3 | 4 | Usage 5 | ----- 6 | 7 | Usage in this blogpost: [Running Caddy Server as a service with Upstart](https://denbeke.be/blog/servers/running-caddy-server-as-a-service/). 8 | Short recap: 9 | 10 | * Download Caddy in `/usr/local/bin/caddy` and execute `sudo setcap cap_net_bind_service=+ep /usr/local/bin/caddy`. 11 | * Save the appropriate upstart config file in `/etc/init/caddy.conf`. 12 | * Ensure that the folder `/etc/caddy` exists and that the subfolder .caddy is owned by `www-data`. 13 | * Create a Caddyfile in `/etc/caddy/Caddyfile`. 14 | * Now you can use `sudo service caddy start|stop|restart`. 15 | -------------------------------------------------------------------------------- /dist/init/linux-upstart/caddy.conf.ubuntu-12.04: -------------------------------------------------------------------------------- 1 | description "Caddy HTTP/2 web server" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [016] 5 | 6 | console log 7 | 8 | setuid www-data 9 | setgid www-data 10 | 11 | respawn 12 | respawn limit 10 5 13 | 14 | # 12.04 upstart version does not support reload 15 | #reload signal SIGUSR1 16 | 17 | # Let's Encrypt certificates will be written to this directory. 18 | env CADDYPATH=/etc/ssl/caddy 19 | 20 | limit nofile 1048576 1048576 21 | 22 | script 23 | cd /etc/ssl/caddy 24 | rootdir="$(mktemp -d -t "caddy-run.XXXXXX")" 25 | exec /usr/local/bin/caddy -agree -log=stdout -conf=/etc/caddy/Caddyfile -root=$rootdir 26 | end script 27 | -------------------------------------------------------------------------------- /caddyhttp/bind/bind_test.go: -------------------------------------------------------------------------------- 1 | package bind 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetupBind(t *testing.T) { 11 | c := caddy.NewTestController("http", `bind 1.2.3.4`) 12 | err := setupBind(c) 13 | if err != nil { 14 | t.Fatalf("Expected no errors, but got: %v", err) 15 | } 16 | 17 | cfg := httpserver.GetConfig(c) 18 | if got, want := cfg.ListenHost, "1.2.3.4"; got != want { 19 | t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got) 20 | } 21 | if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want { 22 | t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | 3 | os: Windows Server 2012 R2 4 | 5 | clone_folder: c:\gopath\src\github.com\mholt\caddy 6 | 7 | environment: 8 | GOPATH: c:\gopath 9 | 10 | install: 11 | - rmdir c:\go /s /q 12 | - appveyor DownloadFile https://storage.googleapis.com/golang/go1.8.windows-amd64.zip 13 | - 7z x go1.8.windows-amd64.zip -y -oC:\ > NUL 14 | - go version 15 | - go env 16 | - go get -t ./... 17 | - go get github.com/golang/lint/golint 18 | - go get github.com/gordonklaus/ineffassign 19 | - set PATH=%GOPATH%\bin;%PATH% 20 | 21 | build: off 22 | 23 | test_script: 24 | - go vet ./... 25 | - go test -race ./... 26 | - ineffassign . 27 | 28 | after_test: 29 | - golint ./... 30 | 31 | deploy: off 32 | -------------------------------------------------------------------------------- /caddyhttp/pprof/setup_test.go: -------------------------------------------------------------------------------- 1 | package pprof 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | ) 8 | 9 | func TestSetup(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | shouldErr bool 13 | }{ 14 | {`pprof`, false}, 15 | {`pprof {}`, true}, 16 | {`pprof /foo`, true}, 17 | {`pprof { 18 | a b 19 | }`, true}, 20 | {`pprof 21 | pprof`, true}, 22 | } 23 | for i, test := range tests { 24 | c := caddy.NewTestController("http", test.input) 25 | err := setup(c) 26 | if test.shouldErr && err == nil { 27 | t.Errorf("Test %v: Expected error but found nil", i) 28 | } else if !test.shouldErr && err != nil { 29 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dist/init/linux-upstart/caddy.conf.centos-6: -------------------------------------------------------------------------------- 1 | description "Caddy HTTP/2 web server" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [016] 5 | 6 | # centos 6 upstart version does not support console 7 | console log 8 | 9 | # centos 6 upstart version does not support setuid/setgid 10 | setuid www-data 11 | setgid www-data 12 | 13 | respawn 14 | respawn limit 10 5 15 | 16 | # centos 6 upstart version does not support reload 17 | reload signal SIGUSR1 18 | 19 | # Let's Encrypt certificates will be written to this directory. 20 | env CADDYPATH=/etc/ssl/caddy 21 | 22 | limit nofile 1048576 1048576 23 | 24 | script 25 | cd /etc/ssl/caddy 26 | rootdir="$(mktemp -d -t "caddy-run.XXXXXX")" 27 | exec /usr/local/bin/caddy -agree -log=stdout -conf=/etc/caddy/Caddyfile -root=$rootdir 28 | end script 29 | -------------------------------------------------------------------------------- /caddyhttp/httpserver/path.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // Path represents a URI path. 9 | type Path string 10 | 11 | // Matches checks to see if other matches p. 12 | // 13 | // Path matching will probably not always be a direct 14 | // comparison; this method assures that paths can be 15 | // easily and consistently matched. 16 | func (p Path) Matches(other string) bool { 17 | if CaseSensitivePath { 18 | return strings.HasPrefix(string(p), other) 19 | } 20 | return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other)) 21 | } 22 | 23 | // PathMatcher is a Path RequestMatcher. 24 | type PathMatcher string 25 | 26 | // Match satisfies RequestMatcher. 27 | func (p PathMatcher) Match(r *http.Request) bool { 28 | return Path(r.URL.Path).Matches(string(p)) 29 | } 30 | -------------------------------------------------------------------------------- /caddyhttp/proxy/setup.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("proxy", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setup, 12 | }) 13 | } 14 | 15 | // setup configures a new Proxy middleware instance. 16 | func setup(c *caddy.Controller) error { 17 | upstreams, err := NewStaticUpstreams(c.Dispenser, httpserver.GetConfig(c).Host()) 18 | if err != nil { 19 | return err 20 | } 21 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 22 | return Proxy{Next: next, Upstreams: upstreams} 23 | }) 24 | 25 | // Register shutdown handlers. 26 | for _, upstream := range upstreams { 27 | c.OnShutdown(upstream.Stop) 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /caddyhttp/pprof/setup.go: -------------------------------------------------------------------------------- 1 | package pprof 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("pprof", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setup, 12 | }) 13 | } 14 | 15 | // setup returns a new instance of a pprof handler. It accepts no arguments or options. 16 | func setup(c *caddy.Controller) error { 17 | found := false 18 | 19 | for c.Next() { 20 | if found { 21 | return c.Err("pprof can only be specified once") 22 | } 23 | if len(c.RemainingArgs()) != 0 { 24 | return c.ArgErr() 25 | } 26 | if c.NextBlock() { 27 | return c.ArgErr() 28 | } 29 | found = true 30 | } 31 | 32 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 33 | return &Handler{Next: next, Mux: NewMux()} 34 | }) 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /caddyhttp/proxy/body.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | ) 8 | 9 | type bufferedBody struct { 10 | *bytes.Reader 11 | } 12 | 13 | func (*bufferedBody) Close() error { 14 | return nil 15 | } 16 | 17 | // rewind allows bufferedBody to be read again. 18 | func (b *bufferedBody) rewind() error { 19 | if b == nil { 20 | return nil 21 | } 22 | _, err := b.Seek(0, io.SeekStart) 23 | return err 24 | } 25 | 26 | // newBufferedBody returns *bufferedBody to use in place of src. Closes src 27 | // and returns Read error on src. All content from src is buffered. 28 | func newBufferedBody(src io.ReadCloser) (*bufferedBody, error) { 29 | if src == nil { 30 | return nil, nil 31 | } 32 | b, err := ioutil.ReadAll(src) 33 | src.Close() 34 | if err != nil { 35 | return nil, err 36 | } 37 | return &bufferedBody{ 38 | Reader: bytes.NewReader(b), 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /caddyhttp/mime/mime.go: -------------------------------------------------------------------------------- 1 | package mime 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | // Config represent a mime config. Map from extension to mime-type. 11 | // Note, this should be safe with concurrent read access, as this is 12 | // not modified concurrently. 13 | type Config map[string]string 14 | 15 | // Mime sets Content-Type header of requests based on configurations. 16 | type Mime struct { 17 | Next httpserver.Handler 18 | Configs Config 19 | } 20 | 21 | // ServeHTTP implements the httpserver.Handler interface. 22 | func (e Mime) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 23 | // Get a clean /-path, grab the extension 24 | ext := path.Ext(path.Clean(r.URL.Path)) 25 | 26 | if contentType, ok := e.Configs[ext]; ok { 27 | w.Header().Set("Content-Type", contentType) 28 | } 29 | 30 | return e.Next.ServeHTTP(w, r) 31 | } 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # shell scripts should not use tabs to indent! 2 | *.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 3 | *.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 4 | 5 | # files for systemd (shell-similar) 6 | *.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 7 | *.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 8 | *.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 9 | 10 | # go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this: 11 | *.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4 12 | 13 | *.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2 14 | .git* text eol=auto core.whitespace whitespace=trailing-space 15 | -------------------------------------------------------------------------------- /assets.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | // AssetsPath returns the path to the folder 10 | // where the application may store data. If 11 | // CADDYPATH env variable is set, that value 12 | // is used. Otherwise, the path is the result 13 | // of evaluating "$HOME/.caddy". 14 | func AssetsPath() string { 15 | if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" { 16 | return caddyPath 17 | } 18 | return filepath.Join(userHomeDir(), ".caddy") 19 | } 20 | 21 | // userHomeDir returns the user's home directory according to 22 | // environment variables. 23 | // 24 | // Credit: http://stackoverflow.com/a/7922977/1048862 25 | func userHomeDir() string { 26 | if runtime.GOOS == "windows" { 27 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 28 | if home == "" { 29 | home = os.Getenv("USERPROFILE") 30 | } 31 | return home 32 | } 33 | return os.Getenv("HOME") 34 | } 35 | -------------------------------------------------------------------------------- /caddyhttp/internalsrv/setup.go: -------------------------------------------------------------------------------- 1 | package internalsrv 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("internal", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setup, 12 | }) 13 | } 14 | 15 | // Internal configures a new Internal middleware instance. 16 | func setup(c *caddy.Controller) error { 17 | paths, err := internalParse(c) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 23 | return Internal{Next: next, Paths: paths} 24 | }) 25 | 26 | return nil 27 | } 28 | 29 | func internalParse(c *caddy.Controller) ([]string, error) { 30 | var paths []string 31 | 32 | for c.Next() { 33 | if !c.NextArg() { 34 | return paths, c.ArgErr() 35 | } 36 | paths = append(paths, c.Val()) 37 | } 38 | 39 | return paths, nil 40 | } 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | - tip 6 | 7 | matrix: 8 | allow_failures: 9 | - go: tip 10 | fast_finish: true 11 | 12 | before_install: 13 | # Decrypts a script that installs an authenticated cookie 14 | # for git to use when cloning from googlesource.com. 15 | # Bypasses "bandwidth limit exceeded" errors. 16 | # See github.com/golang/go/issues/12933 17 | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_3df18f9af81d_key -iv $encrypted_3df18f9af81d_iv -in dist/gitcookie.sh.enc -out dist/gitcookie.sh -d; fi 18 | 19 | install: 20 | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi 21 | - go get -t ./... 22 | - go get github.com/golang/lint/golint 23 | - go get github.com/gordonklaus/ineffassign 24 | - go get github.com/client9/misspell/cmd/misspell 25 | 26 | script: 27 | - diff <(echo -n) <(gofmt -s -d .) 28 | - ineffassign . 29 | - misspell -error . 30 | - go vet ./... 31 | - go test -race ./... 32 | 33 | after_script: 34 | - golint ./... 35 | -------------------------------------------------------------------------------- /caddyhttp/index/index_test.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/staticfiles" 8 | ) 9 | 10 | func TestIndexIncompleteParams(t *testing.T) { 11 | c := caddy.NewTestController("", "index") 12 | 13 | err := setupIndex(c) 14 | if err == nil { 15 | t.Error("Expected an error, but didn't get one") 16 | } 17 | } 18 | 19 | func TestIndex(t *testing.T) { 20 | c := caddy.NewTestController("", "index a.html b.html c.html") 21 | 22 | err := setupIndex(c) 23 | if err != nil { 24 | t.Errorf("Expected no errors, got: %v", err) 25 | } 26 | 27 | expectedIndex := []string{"a.html", "b.html", "c.html"} 28 | 29 | if len(staticfiles.IndexPages) != 3 { 30 | t.Errorf("Expected 3 values, got %v", len(staticfiles.IndexPages)) 31 | } 32 | 33 | // Ensure ordering is correct 34 | for i, actual := range staticfiles.IndexPages { 35 | if actual != expectedIndex[i] { 36 | t.Errorf("Expected value in position %d to be %v, got %v", i, expectedIndex[i], actual) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /caddyhttp/expvar/expvar_test.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | func TestExpVar(t *testing.T) { 13 | rw := ExpVar{ 14 | Next: httpserver.HandlerFunc(contentHandler), 15 | Resource: "/d/v", 16 | } 17 | 18 | tests := []struct { 19 | from string 20 | result int 21 | }{ 22 | {"/d/v", 0}, 23 | {"/x/y", http.StatusOK}, 24 | } 25 | 26 | for i, test := range tests { 27 | req, err := http.NewRequest("GET", test.from, nil) 28 | if err != nil { 29 | t.Fatalf("Test %d: Could not create HTTP request %v", i, err) 30 | } 31 | rec := httptest.NewRecorder() 32 | result, err := rw.ServeHTTP(rec, req) 33 | if err != nil { 34 | t.Fatalf("Test %d: Could not ServeHTTP %v", i, err) 35 | } 36 | if result != test.result { 37 | t.Errorf("Test %d: Expected Header '%d' but was '%d'", 38 | i, test.result, result) 39 | } 40 | } 41 | } 42 | 43 | func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) { 44 | fmt.Fprintf(w, r.URL.String()) 45 | return http.StatusOK, nil 46 | } 47 | -------------------------------------------------------------------------------- /caddyhttp/markdown/metadata/metadata_yaml.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bytes" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | // YAMLParser is the Parser for YAML 10 | type YAMLParser struct { 11 | metadata Metadata 12 | markdown *bytes.Buffer 13 | } 14 | 15 | // Type returns the kind of metadata parser. 16 | func (y *YAMLParser) Type() string { 17 | return "YAML" 18 | } 19 | 20 | // Init prepares the metadata parser for parsing. 21 | func (y *YAMLParser) Init(b *bytes.Buffer) bool { 22 | meta, data := splitBuffer(b, "---") 23 | if meta == nil || data == nil { 24 | return false 25 | } 26 | y.markdown = data 27 | 28 | m := make(map[string]interface{}) 29 | if err := yaml.Unmarshal(meta.Bytes(), &m); err != nil { 30 | return false 31 | } 32 | y.metadata = NewMetadata(m) 33 | 34 | return true 35 | } 36 | 37 | // Metadata returns parsed metadata. It should be called 38 | // only after a call to Parse returns without error. 39 | func (y *YAMLParser) Metadata() Metadata { 40 | return y.metadata 41 | } 42 | 43 | // Markdown renders the text as a byte array 44 | func (y *YAMLParser) Markdown() []byte { 45 | return y.markdown.Bytes() 46 | } 47 | -------------------------------------------------------------------------------- /dist/README.txt: -------------------------------------------------------------------------------- 1 | CADDY 0.9.5 2 | 3 | Website 4 | https://caddyserver.com 5 | 6 | Community 7 | https://forum.caddyserver.com 8 | 9 | Twitter 10 | @caddyserver 11 | 12 | Source Code 13 | https://github.com/mholt/caddy 14 | https://github.com/caddyserver 15 | 16 | 17 | For instructions on using Caddy, please see the user guide on 18 | the website. For a list of what's new in this version, see 19 | CHANGES.txt. 20 | 21 | The Caddy project accepts pull requests. That means you can make 22 | changes to the code and submit it for review, and if it's good, 23 | we'll use it! You can help thousands of Caddy users and level 24 | up your Go programming game by contributing to Caddy's source. 25 | 26 | To report bugs or request features, open an issue on GitHub. 27 | 28 | Want to support the project financially? Consider donating, 29 | especially if your company is using Caddy. Believe me, your 30 | contributions do not go unnoticed! We also have sponsorship 31 | opportunities available. 32 | 33 | For a good time, follow @mholt6 on Twitter. 34 | 35 | And thanks - you're awesome! 36 | 37 | 38 | --- 39 | (c) 2015-2017 Matthew Holt 40 | -------------------------------------------------------------------------------- /caddyhttp/markdown/metadata/metadata_none.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // NoneParser is the parser for plaintext markdown with no metadata. 8 | type NoneParser struct { 9 | metadata Metadata 10 | markdown *bytes.Buffer 11 | } 12 | 13 | // Type returns the kind of parser this struct is. 14 | func (n *NoneParser) Type() string { 15 | return "None" 16 | } 17 | 18 | // Init prepases and parses the metadata and markdown file 19 | func (n *NoneParser) Init(b *bytes.Buffer) bool { 20 | m := make(map[string]interface{}) 21 | n.metadata = NewMetadata(m) 22 | n.markdown = bytes.NewBuffer(b.Bytes()) 23 | 24 | return true 25 | } 26 | 27 | // Parse the metadata 28 | func (n *NoneParser) Parse(b []byte) ([]byte, error) { 29 | return nil, nil 30 | } 31 | 32 | // Metadata returns parsed metadata. It should be called 33 | // only after a call to Parse returns without error. 34 | func (n *NoneParser) Metadata() Metadata { 35 | return n.metadata 36 | } 37 | 38 | // Markdown returns parsed markdown. It should be called 39 | // only after a call to Parse returns without error. 40 | func (n *NoneParser) Markdown() []byte { 41 | return n.markdown.Bytes() 42 | } 43 | -------------------------------------------------------------------------------- /caddyhttp/expvar/setup_test.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | c := caddy.NewTestController("http", `expvar`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, got: %v", err) 15 | } 16 | mids := httpserver.GetConfig(c).Middleware() 17 | if len(mids) == 0 { 18 | t.Fatal("Expected middleware, got 0 instead") 19 | } 20 | 21 | c = caddy.NewTestController("http", `expvar /d/v`) 22 | err = setup(c) 23 | if err != nil { 24 | t.Errorf("Expected no errors, got: %v", err) 25 | } 26 | mids = httpserver.GetConfig(c).Middleware() 27 | if len(mids) == 0 { 28 | t.Fatal("Expected middleware, got 0 instead") 29 | } 30 | 31 | handler := mids[0](httpserver.EmptyNext) 32 | myHandler, ok := handler.(ExpVar) 33 | if !ok { 34 | t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler) 35 | } 36 | if myHandler.Resource != "/d/v" { 37 | t.Errorf("Expected /d/v as expvar resource") 38 | } 39 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 40 | t.Error("'Next' field of handler was not set properly") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /caddyhttp/markdown/metadata/metadata_toml.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/naoina/toml" 7 | ) 8 | 9 | // TOMLParser is the Parser for TOML 10 | type TOMLParser struct { 11 | metadata Metadata 12 | markdown *bytes.Buffer 13 | } 14 | 15 | // Type returns the kind of parser this struct is. 16 | func (t *TOMLParser) Type() string { 17 | return "TOML" 18 | } 19 | 20 | // Init prepares and parses the metadata and markdown file itself 21 | func (t *TOMLParser) Init(b *bytes.Buffer) bool { 22 | meta, data := splitBuffer(b, "+++") 23 | if meta == nil || data == nil { 24 | return false 25 | } 26 | t.markdown = data 27 | 28 | m := make(map[string]interface{}) 29 | if err := toml.Unmarshal(meta.Bytes(), &m); err != nil { 30 | return false 31 | } 32 | t.metadata = NewMetadata(m) 33 | 34 | return true 35 | } 36 | 37 | // Metadata returns parsed metadata. It should be called 38 | // only after a call to Parse returns without error. 39 | func (t *TOMLParser) Metadata() Metadata { 40 | return t.metadata 41 | } 42 | 43 | // Markdown returns parser markdown. It should be called only after a call to Parse returns without error. 44 | func (t *TOMLParser) Markdown() []byte { 45 | return t.markdown.Bytes() 46 | } 47 | -------------------------------------------------------------------------------- /dist/init/mac-launchd/com.caddyserver.web.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.caddyserver.web 7 | EnvironmentVariables 8 | 9 | HOME 10 | /Users/mathias 11 | 12 | ProgramArguments 13 | 14 | sh 15 | -c 16 | ulimit -n 8192; cd /Users/mathias/Sites; ./caddy -agree -email my_email@domain.com -conf=/Users/mathias/Sites/Caddyfile 17 | 18 | UserName 19 | www 20 | RunAtLoad 21 | 22 | KeepAlive 23 | 24 | WorkingDirectory 25 | /Users/mathias/Sites 26 | StandardOutPath 27 | /Users/mathias/Sites/caddy.log 28 | StandardErrorPath 29 | /Users/mathias/Sites/caddy_error.log 30 | 31 | -------------------------------------------------------------------------------- /caddyhttp/httpserver/error.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | _ error = NonHijackerError{} 9 | _ error = NonFlusherError{} 10 | _ error = NonCloseNotifierError{} 11 | ) 12 | 13 | // NonHijackerError is more descriptive error caused by a non hijacker 14 | type NonHijackerError struct { 15 | // underlying type which doesn't implement Hijack 16 | Underlying interface{} 17 | } 18 | 19 | // Implement Error 20 | func (h NonHijackerError) Error() string { 21 | return fmt.Sprintf("%T is not a hijacker", h.Underlying) 22 | } 23 | 24 | // NonFlusherError is more descriptive error caused by a non flusher 25 | type NonFlusherError struct { 26 | // underlying type which doesn't implement Flush 27 | Underlying interface{} 28 | } 29 | 30 | // Implement Error 31 | func (f NonFlusherError) Error() string { 32 | return fmt.Sprintf("%T is not a flusher", f.Underlying) 33 | } 34 | 35 | // NonCloseNotifierError is more descriptive error caused by a non closeNotifier 36 | type NonCloseNotifierError struct { 37 | // underlying type which doesn't implement CloseNotify 38 | Underlying interface{} 39 | } 40 | 41 | // Implement Error 42 | func (c NonCloseNotifierError) Error() string { 43 | return fmt.Sprintf("%T is not a closeNotifier", c.Underlying) 44 | } 45 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | (Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please take a few minutes to carefully answer the following questions. If your issue is not a bug report, you do not need to use this template. Thanks!) 2 | 3 | ### 1. What version of Caddy are you running (`caddy -version`)? 4 | 5 | 6 | ### 2. What are you trying to do? 7 | 8 | 9 | ### 3. What is your entire Caddyfile? 10 | ```text 11 | (Put Caddyfile here) 12 | ``` 13 | 14 | ### 4. How did you run Caddy (give the full command and describe the execution environment)? 15 | 16 | 17 | ### 5. Please paste any relevant HTTP request(s) here. 18 | 19 | (paste curl command, or full HTTP request including headers and body, here) 20 | 21 | 22 | ### 6. What did you expect to see? 23 | 24 | 25 | ### 7. What did you see instead (give full error messages and/or log)? 26 | 27 | 28 | ### 8. How can someone who is starting from scratch reproduce the bug as minimally as possible? 29 | 30 | (Please strip away any extra infrastructure such as containers, reverse proxies, upstream apps, dependencies, etc, to prove this is a bug in Caddy and not an external misconfiguration. Your chances of getting this bug fixed go way up the easier it is to replicate. Thank you!) 31 | -------------------------------------------------------------------------------- /caddy/caddymain/run_test.go: -------------------------------------------------------------------------------- 1 | package caddymain 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func TestSetCPU(t *testing.T) { 9 | currentCPU := runtime.GOMAXPROCS(-1) 10 | maxCPU := runtime.NumCPU() 11 | halfCPU := int(0.5 * float32(maxCPU)) 12 | if halfCPU < 1 { 13 | halfCPU = 1 14 | } 15 | for i, test := range []struct { 16 | input string 17 | output int 18 | shouldErr bool 19 | }{ 20 | {"1", 1, false}, 21 | {"-1", currentCPU, true}, 22 | {"0", currentCPU, true}, 23 | {"100%", maxCPU, false}, 24 | {"50%", halfCPU, false}, 25 | {"110%", currentCPU, true}, 26 | {"-10%", currentCPU, true}, 27 | {"invalid input", currentCPU, true}, 28 | {"invalid input%", currentCPU, true}, 29 | {"9999", maxCPU, false}, // over available CPU 30 | } { 31 | err := setCPU(test.input) 32 | if test.shouldErr && err == nil { 33 | t.Errorf("Test %d: Expected error, but there wasn't any", i) 34 | } 35 | if !test.shouldErr && err != nil { 36 | t.Errorf("Test %d: Expected no error, but there was one: %v", i, err) 37 | } 38 | if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected { 39 | t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected) 40 | } 41 | // teardown 42 | runtime.GOMAXPROCS(currentCPU) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /caddyhttp/markdown/process_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | ) 10 | 11 | func TestConfig_Markdown(t *testing.T) { 12 | tests := []map[string]string{ 13 | {"author": "authorVal"}, 14 | {"copyright": "copyrightVal"}, 15 | {"description": "descriptionVal"}, 16 | {"subject": "subjectVal"}, 17 | {"author": "authorVal", "copyright": "copyrightVal"}, 18 | {"author": "authorVal", "copyright": "copyrightVal", "description": "descriptionVal"}, 19 | {"author": "authorVal", "copyright": "copyrightVal", "description": "descriptionVal", "subject": "subjectVal"}, 20 | } 21 | 22 | for i, meta := range tests { 23 | config := &Config{ 24 | Template: GetDefaultTemplate(), 25 | } 26 | 27 | toml := "+++" 28 | for key, val := range meta { 29 | toml = toml + "\n" + key + "= \"" + val + "\"" 30 | } 31 | toml = toml + "\n+++" 32 | 33 | res, _ := config.Markdown("Test title", strings.NewReader(toml), []os.FileInfo{}, httpserver.Context{}) 34 | sRes := string(res) 35 | 36 | for key, val := range meta { 37 | c := strings.Contains(sRes, "") 38 | if !c { 39 | t.Error("Test case", i, "should contain meta", key, val) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /caddyhttp/expvar/expvar.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "expvar" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | ) 10 | 11 | // ExpVar is a simple struct to hold expvar's configuration 12 | type ExpVar struct { 13 | Next httpserver.Handler 14 | Resource Resource 15 | } 16 | 17 | // ServeHTTP handles requests to expvar's configured entry point with 18 | // expvar, or passes all other requests up the chain. 19 | func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 20 | if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) { 21 | expvarHandler(w, r) 22 | return 0, nil 23 | } 24 | return e.Next.ServeHTTP(w, r) 25 | } 26 | 27 | // expvarHandler returns a JSON object will all the published variables. 28 | // 29 | // This is lifted straight from the expvar package. 30 | func expvarHandler(w http.ResponseWriter, r *http.Request) { 31 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 32 | fmt.Fprintf(w, "{\n") 33 | first := true 34 | expvar.Do(func(kv expvar.KeyValue) { 35 | if !first { 36 | fmt.Fprintf(w, ",\n") 37 | } 38 | first = false 39 | fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) 40 | }) 41 | fmt.Fprintf(w, "\n}\n") 42 | } 43 | 44 | // Resource contains the path to the expvar entry point 45 | type Resource string 46 | -------------------------------------------------------------------------------- /caddyhttp/extensions/setup.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("ext", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setup, 12 | }) 13 | } 14 | 15 | // setup configures a new instance of 'extensions' middleware for clean URLs. 16 | func setup(c *caddy.Controller) error { 17 | cfg := httpserver.GetConfig(c) 18 | root := cfg.Root 19 | 20 | exts, err := extParse(c) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 26 | return Ext{ 27 | Next: next, 28 | Extensions: exts, 29 | Root: root, 30 | } 31 | }) 32 | 33 | return nil 34 | } 35 | 36 | // extParse sets up an instance of extension middleware 37 | // from a middleware controller and returns a list of extensions. 38 | func extParse(c *caddy.Controller) ([]string, error) { 39 | var exts []string 40 | 41 | for c.Next() { 42 | // At least one extension is required 43 | if !c.NextArg() { 44 | return exts, c.ArgErr() 45 | } 46 | exts = append(exts, c.Val()) 47 | 48 | // Tack on any other extensions that may have been listed 49 | exts = append(exts, c.RemainingArgs()...) 50 | } 51 | 52 | return exts, nil 53 | } 54 | -------------------------------------------------------------------------------- /caddyhttp/httpserver/recorder_test.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestNewResponseRecorder(t *testing.T) { 10 | w := httptest.NewRecorder() 11 | recordRequest := NewResponseRecorder(w) 12 | if !(recordRequest.ResponseWriter == w) { 13 | t.Fatalf("Expected Response writer in the Recording to be same as the one sent\n") 14 | } 15 | if recordRequest.status != http.StatusOK { 16 | t.Fatalf("Expected recorded status to be http.StatusOK (%d) , but found %d\n ", http.StatusOK, recordRequest.status) 17 | } 18 | } 19 | func TestWriteHeader(t *testing.T) { 20 | w := httptest.NewRecorder() 21 | recordRequest := NewResponseRecorder(w) 22 | recordRequest.WriteHeader(401) 23 | if w.Code != 401 || recordRequest.status != 401 { 24 | t.Fatalf("Expected Response status to be set to 401, but found %d\n", recordRequest.status) 25 | } 26 | } 27 | 28 | func TestWrite(t *testing.T) { 29 | w := httptest.NewRecorder() 30 | responseTestString := "test" 31 | recordRequest := NewResponseRecorder(w) 32 | buf := []byte(responseTestString) 33 | recordRequest.Write(buf) 34 | if recordRequest.size != len(buf) { 35 | t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size) 36 | } 37 | if w.Body.String() != responseTestString { 38 | t.Fatalf("Expected Response Body to be %s , but found %s\n", responseTestString, w.Body.String()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /caddyhttp/markdown/metadata/metadata_json.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // JSONParser is the MetadataParser for JSON 9 | type JSONParser struct { 10 | metadata Metadata 11 | markdown *bytes.Buffer 12 | } 13 | 14 | // Type returns the kind of metadata parser implemented by this struct. 15 | func (j *JSONParser) Type() string { 16 | return "JSON" 17 | } 18 | 19 | // Init prepares the metadata metadata/markdown file and parses it 20 | func (j *JSONParser) Init(b *bytes.Buffer) bool { 21 | m := make(map[string]interface{}) 22 | 23 | err := json.Unmarshal(b.Bytes(), &m) 24 | if err != nil { 25 | var offset int 26 | 27 | jerr, ok := err.(*json.SyntaxError) 28 | if !ok { 29 | return false 30 | } 31 | 32 | offset = int(jerr.Offset) 33 | 34 | m = make(map[string]interface{}) 35 | err = json.Unmarshal(b.Next(offset-1), &m) 36 | if err != nil { 37 | return false 38 | } 39 | } 40 | 41 | j.metadata = NewMetadata(m) 42 | j.markdown = bytes.NewBuffer(b.Bytes()) 43 | 44 | return true 45 | } 46 | 47 | // Metadata returns parsed metadata. It should be called 48 | // only after a call to Parse returns without error. 49 | func (j *JSONParser) Metadata() Metadata { 50 | return j.metadata 51 | } 52 | 53 | // Markdown returns the markdown text. It should be called only after a call to Parse returns without error. 54 | func (j *JSONParser) Markdown() []byte { 55 | return j.markdown.Bytes() 56 | } 57 | -------------------------------------------------------------------------------- /caddyhttp/caddyhttp.go: -------------------------------------------------------------------------------- 1 | package caddyhttp 2 | 3 | import ( 4 | // plug in the server 5 | _ "github.com/mholt/caddy/caddyhttp/httpserver" 6 | 7 | // plug in the standard directives 8 | _ "github.com/mholt/caddy/caddyhttp/basicauth" 9 | _ "github.com/mholt/caddy/caddyhttp/bind" 10 | _ "github.com/mholt/caddy/caddyhttp/browse" 11 | _ "github.com/mholt/caddy/caddyhttp/errors" 12 | _ "github.com/mholt/caddy/caddyhttp/expvar" 13 | _ "github.com/mholt/caddy/caddyhttp/extensions" 14 | _ "github.com/mholt/caddy/caddyhttp/fastcgi" 15 | _ "github.com/mholt/caddy/caddyhttp/gzip" 16 | _ "github.com/mholt/caddy/caddyhttp/header" 17 | _ "github.com/mholt/caddy/caddyhttp/index" 18 | _ "github.com/mholt/caddy/caddyhttp/internalsrv" 19 | _ "github.com/mholt/caddy/caddyhttp/log" 20 | _ "github.com/mholt/caddy/caddyhttp/markdown" 21 | _ "github.com/mholt/caddy/caddyhttp/maxrequestbody" 22 | _ "github.com/mholt/caddy/caddyhttp/mime" 23 | _ "github.com/mholt/caddy/caddyhttp/pprof" 24 | _ "github.com/mholt/caddy/caddyhttp/proxy" 25 | _ "github.com/mholt/caddy/caddyhttp/push" 26 | _ "github.com/mholt/caddy/caddyhttp/redirect" 27 | _ "github.com/mholt/caddy/caddyhttp/rewrite" 28 | _ "github.com/mholt/caddy/caddyhttp/root" 29 | _ "github.com/mholt/caddy/caddyhttp/status" 30 | _ "github.com/mholt/caddy/caddyhttp/templates" 31 | _ "github.com/mholt/caddy/caddyhttp/timeouts" 32 | _ "github.com/mholt/caddy/caddyhttp/websocket" 33 | _ "github.com/mholt/caddy/startupshutdown" 34 | ) 35 | -------------------------------------------------------------------------------- /caddyhttp/root/root.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/mholt/caddy" 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | ) 10 | 11 | func init() { 12 | caddy.RegisterPlugin("root", caddy.Plugin{ 13 | ServerType: "http", 14 | Action: setupRoot, 15 | }) 16 | } 17 | 18 | func setupRoot(c *caddy.Controller) error { 19 | config := httpserver.GetConfig(c) 20 | 21 | for c.Next() { 22 | if !c.NextArg() { 23 | return c.ArgErr() 24 | } 25 | config.Root = c.Val() 26 | if c.NextArg() { 27 | // only one argument allowed 28 | return c.ArgErr() 29 | } 30 | } 31 | //first check that the path is not a symlink, os.Stat panics when this is true 32 | info, _ := os.Lstat(config.Root) 33 | if info != nil && info.Mode()&os.ModeSymlink == os.ModeSymlink { 34 | //just print out info, delegate responsibility for symlink validity to 35 | //underlying Go framework, no need to test / verify twice 36 | log.Printf("[INFO] Root path is symlink: %s", config.Root) 37 | } else { 38 | // Check if root path exists 39 | _, err := os.Stat(config.Root) 40 | if err != nil { 41 | if os.IsNotExist(err) { 42 | // Allow this, because the folder might appear later. 43 | // But make sure the user knows! 44 | log.Printf("[WARNING] Root path does not exist: %s", config.Root) 45 | } else { 46 | return c.Errf("Unable to access root path '%s': %v", config.Root, err) 47 | } 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /caddytls/httphandler.go: -------------------------------------------------------------------------------- 1 | package caddytls 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "strings" 11 | ) 12 | 13 | const challengeBasePath = "/.well-known/acme-challenge" 14 | 15 | // HTTPChallengeHandler proxies challenge requests to ACME client if the 16 | // request path starts with challengeBasePath. It returns true if it 17 | // handled the request and no more needs to be done; it returns false 18 | // if this call was a no-op and the request still needs handling. 19 | func HTTPChallengeHandler(w http.ResponseWriter, r *http.Request, listenHost, altPort string) bool { 20 | if !strings.HasPrefix(r.URL.Path, challengeBasePath) { 21 | return false 22 | } 23 | if DisableHTTPChallenge { 24 | return false 25 | } 26 | if !namesObtaining.Has(r.Host) { 27 | return false 28 | } 29 | 30 | scheme := "http" 31 | if r.TLS != nil { 32 | scheme = "https" 33 | } 34 | 35 | if listenHost == "" { 36 | listenHost = "localhost" 37 | } 38 | 39 | upstream, err := url.Parse(fmt.Sprintf("%s://%s:%s", scheme, listenHost, altPort)) 40 | if err != nil { 41 | w.WriteHeader(http.StatusInternalServerError) 42 | log.Printf("[ERROR] ACME proxy handler: %v", err) 43 | return true 44 | } 45 | 46 | proxy := httputil.NewSingleHostReverseProxy(upstream) 47 | proxy.Transport = &http.Transport{ 48 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 49 | } 50 | proxy.ServeHTTP(w, r) 51 | 52 | return true 53 | } 54 | -------------------------------------------------------------------------------- /caddytls/storagetest/storagetest_test.go: -------------------------------------------------------------------------------- 1 | package storagetest 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/mholt/caddy/caddytls" 11 | ) 12 | 13 | // TestFileStorage tests the file storage set with the test harness in this 14 | // package. 15 | func TestFileStorage(t *testing.T) { 16 | emailCounter := 0 17 | storageTest := &StorageTest{ 18 | Storage: &caddytls.FileStorage{Path: "./testdata"}, // nameLocks isn't made here, but it's okay because the tests don't call TryLock or Unlock 19 | PostTest: func() { os.RemoveAll("./testdata") }, 20 | AfterUserEmailStore: func(email string) error { 21 | // We need to change the dir mod time to show a 22 | // that certain dirs are newer. 23 | emailCounter++ 24 | fp := filepath.Join("./testdata", "users", email) 25 | 26 | // What we will do is subtract 10 days from today and 27 | // then add counter * seconds to make the later 28 | // counters newer. We accept that this isn't exactly 29 | // how the file storage works because it only changes 30 | // timestamps on *newly seen* users, but it achieves 31 | // the result that the harness expects. 32 | chTime := time.Now().AddDate(0, 0, -10).Add(time.Duration(emailCounter) * time.Second) 33 | if err := os.Chtimes(fp, chTime, chTime); err != nil { 34 | return fmt.Errorf("Unable to change file time for %v: %v", fp, err) 35 | } 36 | return nil 37 | }, 38 | } 39 | storageTest.Test(t, false) 40 | } 41 | -------------------------------------------------------------------------------- /caddyhttp/pprof/pprof_test.go: -------------------------------------------------------------------------------- 1 | package pprof 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | func TestServeHTTP(t *testing.T) { 13 | h := Handler{ 14 | Next: httpserver.HandlerFunc(nextHandler), 15 | Mux: NewMux(), 16 | } 17 | 18 | w := httptest.NewRecorder() 19 | r, err := http.NewRequest("GET", "/debug/pprof", nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | status, err := h.ServeHTTP(w, r) 24 | 25 | if status != 0 { 26 | t.Errorf("Expected status %d but got %d", 0, status) 27 | } 28 | if err != nil { 29 | t.Errorf("Expected nil error, but got: %v", err) 30 | } 31 | if w.Body.String() == "content" { 32 | t.Errorf("Expected pprof to handle request, but it didn't") 33 | } 34 | 35 | w = httptest.NewRecorder() 36 | r, err = http.NewRequest("GET", "/foo", nil) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | status, err = h.ServeHTTP(w, r) 41 | if status != http.StatusNotFound { 42 | t.Errorf("Test two: Expected status %d but got %d", http.StatusNotFound, status) 43 | } 44 | if err != nil { 45 | t.Errorf("Test two: Expected nil error, but got: %v", err) 46 | } 47 | if w.Body.String() != "content" { 48 | t.Errorf("Expected pprof to pass the request through, but it didn't; got: %s", w.Body.String()) 49 | } 50 | } 51 | 52 | func nextHandler(w http.ResponseWriter, r *http.Request) (int, error) { 53 | fmt.Fprintf(w, "content") 54 | return http.StatusNotFound, nil 55 | } 56 | -------------------------------------------------------------------------------- /caddyhttp/rewrite/to_test.go: -------------------------------------------------------------------------------- 1 | package rewrite 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestTo(t *testing.T) { 10 | fs := http.Dir("testdata") 11 | tests := []struct { 12 | url string 13 | to string 14 | expected string 15 | }{ 16 | {"/", "/somefiles", "/somefiles"}, 17 | {"/somefiles", "/somefiles /index.php{uri}", "/index.php/somefiles"}, 18 | {"/somefiles", "/testfile /index.php{uri}", "/testfile"}, 19 | {"/somefiles", "/testfile/ /index.php{uri}", "/index.php/somefiles"}, 20 | {"/somefiles", "/somefiles /index.php{uri}", "/index.php/somefiles"}, 21 | {"/?a=b", "/somefiles /index.php?{query}", "/index.php?a=b"}, 22 | {"/?a=b", "/testfile /index.php?{query}", "/testfile?a=b"}, 23 | {"/?a=b", "/testdir /index.php?{query}", "/index.php?a=b"}, 24 | {"/?a=b", "/testdir/ /index.php?{query}", "/testdir/?a=b"}, 25 | {"/test?url=http://", " /p/{path}?{query}", "/p/test?url=http://"}, 26 | {"/test?url=http://", " /p/{rewrite_path}?{query}", "/p/test?url=http://"}, 27 | {"/test/?url=http://", " /{uri}", "/test/?url=http://"}, 28 | } 29 | 30 | uri := func(r *url.URL) string { 31 | uri := r.Path 32 | if r.RawQuery != "" { 33 | uri += "?" + r.RawQuery 34 | } 35 | return uri 36 | } 37 | for i, test := range tests { 38 | r, err := http.NewRequest("GET", test.url, nil) 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | To(fs, r, test.to, newReplacer(r)) 43 | if uri(r.URL) != test.expected { 44 | t.Errorf("Test %v: expected %v found %v", i, test.expected, uri(r.URL)) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /caddyhttp/httpserver/middleware_test.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestPathCaseSensitivity(t *testing.T) { 9 | tests := []struct { 10 | basePath string 11 | path string 12 | caseSensitive bool 13 | expected bool 14 | }{ 15 | {"/", "/file", true, true}, 16 | {"/a", "/file", true, false}, 17 | {"/f", "/file", true, true}, 18 | {"/f", "/File", true, false}, 19 | {"/f", "/File", false, true}, 20 | {"/file", "/file", true, true}, 21 | {"/file", "/file", false, true}, 22 | {"/files", "/file", false, false}, 23 | {"/files", "/file", true, false}, 24 | {"/folder", "/folder/file.txt", true, true}, 25 | {"/folders", "/folder/file.txt", true, false}, 26 | {"/folder", "/Folder/file.txt", false, true}, 27 | {"/folders", "/Folder/file.txt", false, false}, 28 | } 29 | 30 | for i, test := range tests { 31 | CaseSensitivePath = test.caseSensitive 32 | valid := Path(test.path).Matches(test.basePath) 33 | if test.expected != valid { 34 | t.Errorf("Test %d: Expected %v, found %v", i, test.expected, valid) 35 | } 36 | } 37 | } 38 | 39 | func TestPathCaseSensitiveEnv(t *testing.T) { 40 | tests := []struct { 41 | envValue string 42 | expected bool 43 | }{ 44 | {"1", true}, 45 | {"0", false}, 46 | {"false", false}, 47 | {"true", true}, 48 | {"", true}, 49 | } 50 | 51 | for i, test := range tests { 52 | os.Setenv(caseSensitivePathEnv, test.envValue) 53 | initCaseSettings() 54 | if test.expected != CaseSensitivePath { 55 | t.Errorf("Test %d: Expected %v, found %v", i, test.expected, CaseSensitivePath) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /caddyhttp/extensions/ext.go: -------------------------------------------------------------------------------- 1 | // Package extensions contains middleware for clean URLs. 2 | // 3 | // The root path of the site is passed in as well as possible extensions 4 | // to try internally for paths requested that don't match an existing 5 | // resource. The first path+ext combination that matches a valid file 6 | // will be used. 7 | package extensions 8 | 9 | import ( 10 | "net/http" 11 | "os" 12 | "path" 13 | "strings" 14 | 15 | "github.com/mholt/caddy/caddyhttp/httpserver" 16 | ) 17 | 18 | // Ext can assume an extension from clean URLs. 19 | // It tries extensions in the order listed in Extensions. 20 | type Ext struct { 21 | // Next handler in the chain 22 | Next httpserver.Handler 23 | 24 | // Path to site root 25 | Root string 26 | 27 | // List of extensions to try 28 | Extensions []string 29 | } 30 | 31 | // ServeHTTP implements the httpserver.Handler interface. 32 | func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 33 | urlpath := strings.TrimSuffix(r.URL.Path, "/") 34 | if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' { 35 | for _, ext := range e.Extensions { 36 | if resourceExists(e.Root, urlpath+ext) { 37 | r.URL.Path = urlpath + ext 38 | break 39 | } 40 | } 41 | } 42 | return e.Next.ServeHTTP(w, r) 43 | } 44 | 45 | // resourceExists returns true if the file specified at 46 | // root + path exists; false otherwise. 47 | func resourceExists(root, path string) bool { 48 | _, err := os.Stat(root + path) 49 | // technically we should use os.IsNotExist(err) 50 | // but we don't handle any other kinds of errors anyway 51 | return err == nil 52 | } 53 | -------------------------------------------------------------------------------- /caddyhttp/proxy/body_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | func TestBodyRetry(t *testing.T) { 13 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | io.Copy(w, r.Body) 15 | r.Body.Close() 16 | })) 17 | defer ts.Close() 18 | 19 | testcase := "test content" 20 | req, err := http.NewRequest(http.MethodPost, ts.URL, bytes.NewBufferString(testcase)) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | body, err := newBufferedBody(req.Body) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if body != nil { 30 | req.Body = body 31 | } 32 | 33 | // simulate fail request 34 | host := req.URL.Host 35 | req.URL.Host = "example.com" 36 | body.rewind() 37 | _, _ = http.DefaultTransport.RoundTrip(req) 38 | 39 | // retry request 40 | req.URL.Host = host 41 | body.rewind() 42 | resp, err := http.DefaultTransport.RoundTrip(req) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | result, err := ioutil.ReadAll(resp.Body) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | resp.Body.Close() 51 | if string(result) != testcase { 52 | t.Fatalf("result = %s, want %s", result, testcase) 53 | } 54 | 55 | // try one more time for body reuse 56 | body.rewind() 57 | resp, err = http.DefaultTransport.RoundTrip(req) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | result, err = ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | resp.Body.Close() 66 | if string(result) != testcase { 67 | t.Fatalf("result = %s, want %s", result, testcase) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /caddy/build.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Caddy build script. Automates proper versioning. 4 | # 5 | # Usage: 6 | # 7 | # $ ./build.bash [output_filename] [git_repo] 8 | # 9 | # Outputs compiled program in current directory. 10 | # Default git repo is current directory. 11 | # Builds always take place from current directory. 12 | 13 | set -euo pipefail 14 | 15 | : ${output_filename:="${1:-}"} 16 | : ${output_filename:=""} 17 | 18 | : ${git_repo:="${2:-}"} 19 | : ${git_repo:="."} 20 | 21 | pkg=github.com/mholt/caddy/caddy/caddymain 22 | ldflags=() 23 | 24 | # Timestamp of build 25 | name="${pkg}.buildDate" 26 | value=$(date -u +"%a %b %d %H:%M:%S %Z %Y") 27 | ldflags+=("-X" "\"${name}=${value}\"") 28 | 29 | # Current tag, if HEAD is on a tag 30 | name="${pkg}.gitTag" 31 | set +e 32 | value="$(git -C "${git_repo}" describe --exact-match HEAD 2>/dev/null)" 33 | set -e 34 | ldflags+=("-X" "\"${name}=${value}\"") 35 | 36 | # Nearest tag on branch 37 | name="${pkg}.gitNearestTag" 38 | value="$(git -C "${git_repo}" describe --abbrev=0 --tags HEAD)" 39 | ldflags+=("-X" "\"${name}=${value}\"") 40 | 41 | # Commit SHA 42 | name="${pkg}.gitCommit" 43 | value="$(git -C "${git_repo}" rev-parse --short HEAD)" 44 | ldflags+=("-X" "\"${name}=${value}\"") 45 | 46 | # Summary of uncommitted changes 47 | name="${pkg}.gitShortStat" 48 | value="$(git -C "${git_repo}" diff-index --shortstat HEAD)" 49 | ldflags+=("-X" "\"${name}=${value}\"") 50 | 51 | # List of modified files 52 | name="${pkg}.gitFilesModified" 53 | value="$(git -C "${git_repo}" diff-index --name-only HEAD)" 54 | ldflags+=("-X" "\"${name}=${value}\"") 55 | 56 | go build -ldflags "${ldflags[*]}" -o "${output_filename}" 57 | -------------------------------------------------------------------------------- /caddyhttp/pprof/pprof.go: -------------------------------------------------------------------------------- 1 | package pprof 2 | 3 | import ( 4 | "net/http" 5 | pp "net/http/pprof" 6 | 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | // BasePath is the base path to match for all pprof requests. 11 | const BasePath = "/debug/pprof" 12 | 13 | // Handler is a simple struct whose ServeHTTP will delegate pprof 14 | // endpoints to their equivalent net/http/pprof handlers. 15 | type Handler struct { 16 | Next httpserver.Handler 17 | Mux *http.ServeMux 18 | } 19 | 20 | // ServeHTTP handles requests to BasePath with pprof, or passes 21 | // all other requests up the chain. 22 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 23 | if httpserver.Path(r.URL.Path).Matches(BasePath) { 24 | h.Mux.ServeHTTP(w, r) 25 | return 0, nil 26 | } 27 | return h.Next.ServeHTTP(w, r) 28 | } 29 | 30 | // NewMux returns a new http.ServeMux that routes pprof requests. 31 | // It pretty much copies what the std lib pprof does on init: 32 | // https://golang.org/src/net/http/pprof/pprof.go#L67 33 | func NewMux() *http.ServeMux { 34 | mux := http.NewServeMux() 35 | mux.HandleFunc(BasePath+"/", func(w http.ResponseWriter, r *http.Request) { 36 | // this endpoint, as implemented in the standard library, doesn't set 37 | // its Content-Type header, so using this can confuse clients, especially 38 | // if gzipping... 39 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 40 | pp.Index(w, r) 41 | }) 42 | mux.HandleFunc(BasePath+"/cmdline", pp.Cmdline) 43 | mux.HandleFunc(BasePath+"/profile", pp.Profile) 44 | mux.HandleFunc(BasePath+"/symbol", pp.Symbol) 45 | mux.HandleFunc(BasePath+"/trace", pp.Trace) 46 | return mux 47 | } 48 | -------------------------------------------------------------------------------- /caddyhttp/status/status.go: -------------------------------------------------------------------------------- 1 | // Package status is middleware for returning status code for requests 2 | package status 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | // Rule describes status rewriting rule 11 | type Rule struct { 12 | // Base path. Request to this path and sub-paths will be answered with StatusCode 13 | Base string 14 | 15 | // Status code to return 16 | StatusCode int 17 | 18 | // Request matcher 19 | httpserver.RequestMatcher 20 | } 21 | 22 | // NewRule creates new Rule. 23 | func NewRule(basePath string, status int) *Rule { 24 | return &Rule{ 25 | Base: basePath, 26 | StatusCode: status, 27 | RequestMatcher: httpserver.PathMatcher(basePath), 28 | } 29 | } 30 | 31 | // BasePath implements httpserver.HandlerConfig interface 32 | func (rule *Rule) BasePath() string { 33 | return rule.Base 34 | } 35 | 36 | // Status is a middleware to return status code for request 37 | type Status struct { 38 | Rules []httpserver.HandlerConfig 39 | Next httpserver.Handler 40 | } 41 | 42 | // ServeHTTP implements the httpserver.Handler interface 43 | func (status Status) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 44 | if cfg := httpserver.ConfigSelector(status.Rules).Select(r); cfg != nil { 45 | rule := cfg.(*Rule) 46 | 47 | if rule.StatusCode < 400 { 48 | // There's no ability to return response body -- 49 | // write the response status code in header and signal 50 | // to other handlers that response is already handled 51 | w.WriteHeader(rule.StatusCode) 52 | return 0, nil 53 | } 54 | 55 | return rule.StatusCode, nil 56 | } 57 | 58 | return status.Next.ServeHTTP(w, r) 59 | } 60 | -------------------------------------------------------------------------------- /caddyhttp/expvar/setup.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "expvar" 5 | "runtime" 6 | "sync" 7 | 8 | "github.com/mholt/caddy" 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | func init() { 13 | caddy.RegisterPlugin("expvar", caddy.Plugin{ 14 | ServerType: "http", 15 | Action: setup, 16 | }) 17 | } 18 | 19 | // setup configures a new ExpVar middleware instance. 20 | func setup(c *caddy.Controller) error { 21 | resource, err := expVarParse(c) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // publish any extra information/metrics we may want to capture 27 | publishExtraVars() 28 | 29 | ev := ExpVar{Resource: resource} 30 | 31 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 32 | ev.Next = next 33 | return ev 34 | }) 35 | 36 | return nil 37 | } 38 | 39 | func expVarParse(c *caddy.Controller) (Resource, error) { 40 | var resource Resource 41 | var err error 42 | 43 | for c.Next() { 44 | args := c.RemainingArgs() 45 | switch len(args) { 46 | case 0: 47 | resource = Resource(defaultExpvarPath) 48 | case 1: 49 | resource = Resource(args[0]) 50 | default: 51 | return resource, c.ArgErr() 52 | } 53 | } 54 | 55 | return resource, err 56 | } 57 | 58 | func publishExtraVars() { 59 | // By using sync.Once instead of an init() function, we don't clutter 60 | // the app's expvar export unnecessarily, or risk colliding with it. 61 | publishOnce.Do(func() { 62 | expvar.Publish("Goroutines", expvar.Func(func() interface{} { 63 | return runtime.NumGoroutine() 64 | })) 65 | }) 66 | } 67 | 68 | var publishOnce sync.Once // publishing variables should only be done once 69 | var defaultExpvarPath = "/debug/vars" 70 | -------------------------------------------------------------------------------- /caddyhttp/mime/mime_test.go: -------------------------------------------------------------------------------- 1 | package mime 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | func TestMimeHandler(t *testing.T) { 13 | mimes := Config{ 14 | ".html": "text/html", 15 | ".txt": "text/plain", 16 | ".swf": "application/x-shockwave-flash", 17 | } 18 | 19 | m := Mime{Configs: mimes} 20 | 21 | w := httptest.NewRecorder() 22 | exts := []string{ 23 | ".html", ".txt", ".swf", 24 | } 25 | for _, e := range exts { 26 | url := "/file" + e 27 | r, err := http.NewRequest("GET", url, nil) 28 | if err != nil { 29 | t.Error(err) 30 | } 31 | m.Next = nextFunc(true, mimes[e]) 32 | _, err = m.ServeHTTP(w, r) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | } 37 | 38 | w = httptest.NewRecorder() 39 | exts = []string{ 40 | ".htm1", ".abc", ".mdx", 41 | } 42 | for _, e := range exts { 43 | url := "/file" + e 44 | r, err := http.NewRequest("GET", url, nil) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | m.Next = nextFunc(false, "") 49 | _, err = m.ServeHTTP(w, r) 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | } 54 | } 55 | 56 | func nextFunc(shouldMime bool, contentType string) httpserver.Handler { 57 | return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 58 | if shouldMime { 59 | if w.Header().Get("Content-Type") != contentType { 60 | return 0, fmt.Errorf("expected Content-Type: %v, found %v", contentType, r.Header.Get("Content-Type")) 61 | } 62 | return 0, nil 63 | } 64 | if w.Header().Get("Content-Type") != "" { 65 | return 0, fmt.Errorf("Content-Type header not expected") 66 | } 67 | return 0, nil 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /dist/init/README.md: -------------------------------------------------------------------------------- 1 | Init/Service Scripts 2 | ==================== 3 | 4 | This folder contains init/service scripts for using Caddy on various Linux and BSD distributions. They are created and maintained by the community. 5 | 6 | ## Getting Help 7 | 8 | Different scripts have different maintainers; please consult the comments in the file and any README for assistance setting it up. Do not open an issue on the Caddy project about these scripts; instead, to ask a question or suggest a change, please contact the maintainer of the script directly. 9 | 10 | ## Disclaimer 11 | 12 | The files contained herein are not officially supported by the Caddy project author and/or contributors, and as such, the files are not endorsed by the same. The Caddy project author and its contributors are not responsible for the function or malfunction of these scripts/files, or any unintended consequences to your system or website in attempting to set up Caddy. Users are expected to know how to administer their system, and these files should be considered as only a guide or suggestion for using Caddy in certain environments. 13 | 14 | ## Guidelines 15 | 16 | The files distributed here should adhere to these principles where relevant (adjust accordingly for each system/platform): 17 | 18 | - Don't run as root. 19 | - Create a no-shell default user to run it. 20 | - Raise file descriptor limits. 21 | - Don't restart endlessly; if Caddy fails to start, there's a reason -- fix it, don't hammer it. 22 | - Allow Caddy to re-use the same, persistent folder for storage. 23 | - Stay as simple and minimal as possible. 24 | - Be idempotent. 25 | - Use comments to explain unexpected or unusual lines/patterns. 26 | - Be secure by default. 27 | 28 | Thank you for using Caddy! May it serve you well. 29 | -------------------------------------------------------------------------------- /caddyhttp/mime/setup_test.go: -------------------------------------------------------------------------------- 1 | package mime 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | c := caddy.NewTestController("http", `mime .txt text/plain`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, but got: %v", err) 15 | } 16 | mids := httpserver.GetConfig(c).Middleware() 17 | if len(mids) == 0 { 18 | t.Fatal("Expected middleware, but had 0 instead") 19 | } 20 | 21 | handler := mids[0](httpserver.EmptyNext) 22 | myHandler, ok := handler.(Mime) 23 | if !ok { 24 | t.Fatalf("Expected handler to be type Mime, got: %#v", handler) 25 | } 26 | 27 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 28 | t.Error("'Next' field of handler was not set properly") 29 | } 30 | 31 | tests := []struct { 32 | input string 33 | shouldErr bool 34 | }{ 35 | {`mime {`, true}, 36 | {`mime {}`, true}, 37 | {`mime a b`, true}, 38 | {`mime a {`, true}, 39 | {`mime { txt f } `, true}, 40 | {`mime { html } `, true}, 41 | {`mime { 42 | .html text/html 43 | .txt text/plain 44 | } `, false}, 45 | {`mime { 46 | .foo text/foo 47 | .bar text/bar 48 | .foo text/foobar 49 | } `, true}, 50 | {`mime { .html text/html } `, false}, 51 | {`mime { .html 52 | } `, true}, 53 | {`mime .txt text/plain`, false}, 54 | } 55 | for i, test := range tests { 56 | m, err := mimeParse(caddy.NewTestController("http", test.input)) 57 | if test.shouldErr && err == nil { 58 | t.Errorf("Test %v: Expected error but found nil %v", i, m) 59 | } else if !test.shouldErr && err != nil { 60 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /caddyhttp/mime/setup.go: -------------------------------------------------------------------------------- 1 | package mime 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mholt/caddy" 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | ) 10 | 11 | func init() { 12 | caddy.RegisterPlugin("mime", caddy.Plugin{ 13 | ServerType: "http", 14 | Action: setup, 15 | }) 16 | } 17 | 18 | // setup configures a new mime middleware instance. 19 | func setup(c *caddy.Controller) error { 20 | configs, err := mimeParse(c) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 26 | return Mime{Next: next, Configs: configs} 27 | }) 28 | 29 | return nil 30 | } 31 | 32 | func mimeParse(c *caddy.Controller) (Config, error) { 33 | configs := Config{} 34 | 35 | for c.Next() { 36 | // At least one extension is required 37 | 38 | args := c.RemainingArgs() 39 | switch len(args) { 40 | case 2: 41 | if err := validateExt(configs, args[0]); err != nil { 42 | return configs, err 43 | } 44 | configs[args[0]] = args[1] 45 | case 1: 46 | return configs, c.ArgErr() 47 | case 0: 48 | for c.NextBlock() { 49 | ext := c.Val() 50 | if err := validateExt(configs, ext); err != nil { 51 | return configs, err 52 | } 53 | if !c.NextArg() { 54 | return configs, c.ArgErr() 55 | } 56 | configs[ext] = c.Val() 57 | } 58 | } 59 | 60 | } 61 | 62 | return configs, nil 63 | } 64 | 65 | // validateExt checks for valid file name extension. 66 | func validateExt(configs Config, ext string) error { 67 | if !strings.HasPrefix(ext, ".") { 68 | return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext) 69 | } 70 | if _, ok := configs[ext]; ok { 71 | return fmt.Errorf(`mime: duplicate extension "%v" found`, ext) 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /caddyhttp/redirect/redirect.go: -------------------------------------------------------------------------------- 1 | // Package redirect is middleware for redirecting certain requests 2 | // to other locations. 3 | package redirect 4 | 5 | import ( 6 | "fmt" 7 | "html" 8 | "net/http" 9 | 10 | "github.com/mholt/caddy/caddyhttp/httpserver" 11 | ) 12 | 13 | // Redirect is middleware to respond with HTTP redirects 14 | type Redirect struct { 15 | Next httpserver.Handler 16 | Rules []Rule 17 | } 18 | 19 | // ServeHTTP implements the httpserver.Handler interface. 20 | func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 21 | for _, rule := range rd.Rules { 22 | if (rule.FromPath == "/" || r.URL.Path == rule.FromPath) && schemeMatches(rule, r) && rule.Match(r) { 23 | to := httpserver.NewReplacer(r, nil, "").Replace(rule.To) 24 | if rule.Meta { 25 | safeTo := html.EscapeString(to) 26 | fmt.Fprintf(w, metaRedir, safeTo, safeTo) 27 | } else { 28 | http.Redirect(w, r, to, rule.Code) 29 | } 30 | return 0, nil 31 | } 32 | } 33 | return rd.Next.ServeHTTP(w, r) 34 | } 35 | 36 | func schemeMatches(rule Rule, req *http.Request) bool { 37 | return (rule.FromScheme() == "https" && req.TLS != nil) || 38 | (rule.FromScheme() != "https" && req.TLS == nil) 39 | } 40 | 41 | // Rule describes an HTTP redirect rule. 42 | type Rule struct { 43 | FromScheme func() string 44 | FromPath, To string 45 | Code int 46 | Meta bool 47 | httpserver.RequestMatcher 48 | } 49 | 50 | // Script tag comes first since that will better imitate a redirect in the browser's 51 | // history, but the meta tag is a fallback for most non-JS clients. 52 | const metaRedir = ` 53 | 54 | 55 | 56 | 57 | 58 | Redirecting... 59 | 60 | ` 61 | -------------------------------------------------------------------------------- /dist/init/linux-systemd/caddy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Caddy HTTP/2 web server 3 | Documentation=https://caddyserver.com/docs 4 | After=network-online.target 5 | Wants=network-online.target systemd-networkd-wait-online.service 6 | 7 | [Service] 8 | Restart=on-failure 9 | StartLimitInterval=86400 10 | StartLimitBurst=5 11 | 12 | ; User and group the process will run as. 13 | User=www-data 14 | Group=www-data 15 | 16 | ; Letsencrypt-issued certificates will be written to this directory. 17 | Environment=CADDYPATH=/etc/ssl/caddy 18 | 19 | ; Always set "-root" to something safe in case it gets forgotten in the Caddyfile. 20 | ExecStart=/usr/local/bin/caddy -log stdout -agree=true -conf=/etc/caddy/Caddyfile -root=/var/tmp 21 | ExecReload=/bin/kill -USR1 $MAINPID 22 | 23 | ; Limit the number of file descriptors; see `man systemd.exec` for more limit settings. 24 | LimitNOFILE=1048576 25 | ; Unmodified caddy is not expected to use more than that. 26 | LimitNPROC=64 27 | 28 | ; Use private /tmp and /var/tmp, which are discarded after caddy stops. 29 | PrivateTmp=true 30 | ; Use a minimal /dev 31 | PrivateDevices=true 32 | ; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys. 33 | ProtectHome=true 34 | ; Make /usr, /boot, /etc and possibly some more folders read-only. 35 | ProtectSystem=full 36 | ; … except /etc/ssl/caddy, because we want Letsencrypt-certificates there. 37 | ; This merely retains r/w access rights, it does not add any new. Must still be writable on the host! 38 | ReadWriteDirectories=/etc/ssl/caddy 39 | 40 | ; The following additional security directives only work with systemd v229 or later. 41 | ; They further retrict privileges that can be gained by caddy. Uncomment if you like. 42 | ; Note that you may have to add capabilities required by any plugins in use. 43 | ;CapabilityBoundingSet=CAP_NET_BIND_SERVICE 44 | ;AmbientCapabilities=CAP_NET_BIND_SERVICE 45 | ;NoNewPrivileges=true 46 | 47 | [Install] 48 | WantedBy=multi-user.target 49 | -------------------------------------------------------------------------------- /startupshutdown/startupshutdown.go: -------------------------------------------------------------------------------- 1 | package startupshutdown 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/mholt/caddy" 10 | ) 11 | 12 | func init() { 13 | caddy.RegisterPlugin("startup", caddy.Plugin{Action: Startup}) 14 | caddy.RegisterPlugin("shutdown", caddy.Plugin{Action: Shutdown}) 15 | } 16 | 17 | // Startup registers a startup callback to execute during server start. 18 | func Startup(c *caddy.Controller) error { 19 | return registerCallback(c, c.OnFirstStartup) 20 | } 21 | 22 | // Shutdown registers a shutdown callback to execute during server stop. 23 | func Shutdown(c *caddy.Controller) error { 24 | return registerCallback(c, c.OnFinalShutdown) 25 | } 26 | 27 | // registerCallback registers a callback function to execute by 28 | // using c to parse the directive. It registers the callback 29 | // to be executed using registerFunc. 30 | func registerCallback(c *caddy.Controller, registerFunc func(func() error)) error { 31 | var funcs []func() error 32 | 33 | for c.Next() { 34 | args := c.RemainingArgs() 35 | if len(args) == 0 { 36 | return c.ArgErr() 37 | } 38 | 39 | nonblock := false 40 | if len(args) > 1 && args[len(args)-1] == "&" { 41 | // Run command in background; non-blocking 42 | nonblock = true 43 | args = args[:len(args)-1] 44 | } 45 | 46 | command, args, err := caddy.SplitCommandAndArgs(strings.Join(args, " ")) 47 | if err != nil { 48 | return c.Err(err.Error()) 49 | } 50 | 51 | fn := func() error { 52 | cmd := exec.Command(command, args...) 53 | cmd.Stdin = os.Stdin 54 | cmd.Stdout = os.Stdout 55 | cmd.Stderr = os.Stderr 56 | if nonblock { 57 | log.Printf("[INFO] Nonblocking Command:\"%s %s\"", command, strings.Join(args, " ")) 58 | return cmd.Start() 59 | } 60 | log.Printf("[INFO] Blocking Command:\"%s %s\"", command, strings.Join(args, " ")) 61 | return cmd.Run() 62 | } 63 | 64 | funcs = append(funcs, fn) 65 | } 66 | 67 | return c.OncePerServerBlock(func() error { 68 | for _, fn := range funcs { 69 | registerFunc(fn) 70 | } 71 | return nil 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /startupshutdown/startupshutdown_test.go: -------------------------------------------------------------------------------- 1 | package startupshutdown 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/mholt/caddy" 11 | ) 12 | 13 | // The Startup function's tests are symmetrical to Shutdown tests, 14 | // because the Startup and Shutdown functions share virtually the 15 | // same functionality 16 | func TestStartup(t *testing.T) { 17 | tempDirPath := os.TempDir() 18 | 19 | testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown") 20 | defer func() { 21 | // clean up after non-blocking startup function quits 22 | time.Sleep(500 * time.Millisecond) 23 | os.RemoveAll(testDir) 24 | }() 25 | osSenitiveTestDir := filepath.FromSlash(testDir) 26 | os.RemoveAll(osSenitiveTestDir) // start with a clean slate 27 | 28 | var registeredFunction func() error 29 | fakeRegister := func(fn func() error) { 30 | registeredFunction = fn 31 | } 32 | 33 | tests := []struct { 34 | input string 35 | shouldExecutionErr bool 36 | shouldRemoveErr bool 37 | }{ 38 | // test case #0 tests proper functionality blocking commands 39 | {"startup mkdir " + osSenitiveTestDir, false, false}, 40 | 41 | // test case #1 tests proper functionality of non-blocking commands 42 | {"startup mkdir " + osSenitiveTestDir + " &", false, true}, 43 | 44 | // test case #2 tests handling of non-existent commands 45 | {"startup " + strconv.Itoa(int(time.Now().UnixNano())), true, true}, 46 | } 47 | 48 | for i, test := range tests { 49 | c := caddy.NewTestController("", test.input) 50 | err := registerCallback(c, fakeRegister) 51 | if err != nil { 52 | t.Errorf("Expected no errors, got: %v", err) 53 | } 54 | if registeredFunction == nil { 55 | t.Fatalf("Expected function to be registered, but it wasn't") 56 | } 57 | err = registeredFunction() 58 | if err != nil && !test.shouldExecutionErr { 59 | t.Errorf("Test %d received an error of:\n%v", i, err) 60 | } 61 | err = os.Remove(osSenitiveTestDir) 62 | if err != nil && !test.shouldRemoveErr { 63 | t.Errorf("Test %d received an error of:\n%v", i, err) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /caddyhttp/internalsrv/setup_test.go: -------------------------------------------------------------------------------- 1 | package internalsrv 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | c := caddy.NewTestController("http", `internal /internal`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, got: %v", err) 15 | } 16 | mids := httpserver.GetConfig(c).Middleware() 17 | if len(mids) == 0 { 18 | t.Fatal("Expected middleware, got 0 instead") 19 | } 20 | 21 | handler := mids[0](httpserver.EmptyNext) 22 | myHandler, ok := handler.(Internal) 23 | 24 | if !ok { 25 | t.Fatalf("Expected handler to be type Internal, got: %#v", handler) 26 | } 27 | 28 | if myHandler.Paths[0] != "/internal" { 29 | t.Errorf("Expected internal in the list of internal Paths") 30 | } 31 | 32 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 33 | t.Error("'Next' field of handler was not set properly") 34 | } 35 | 36 | } 37 | 38 | func TestInternalParse(t *testing.T) { 39 | tests := []struct { 40 | inputInternalPaths string 41 | shouldErr bool 42 | expectedInternalPaths []string 43 | }{ 44 | {`internal /internal`, false, []string{"/internal"}}, 45 | 46 | {`internal /internal1 47 | internal /internal2`, false, []string{"/internal1", "/internal2"}}, 48 | } 49 | for i, test := range tests { 50 | actualInternalPaths, err := internalParse(caddy.NewTestController("http", test.inputInternalPaths)) 51 | 52 | if err == nil && test.shouldErr { 53 | t.Errorf("Test %d didn't error, but it should have", i) 54 | } else if err != nil && !test.shouldErr { 55 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 56 | } 57 | 58 | if len(actualInternalPaths) != len(test.expectedInternalPaths) { 59 | t.Fatalf("Test %d expected %d InternalPaths, but got %d", 60 | i, len(test.expectedInternalPaths), len(actualInternalPaths)) 61 | } 62 | for j, actualInternalPath := range actualInternalPaths { 63 | if actualInternalPath != test.expectedInternalPaths[j] { 64 | t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s", 65 | i, j, test.expectedInternalPaths[j], actualInternalPath) 66 | } 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /caddyhttp/header/setup.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func init() { 11 | caddy.RegisterPlugin("header", caddy.Plugin{ 12 | ServerType: "http", 13 | Action: setup, 14 | }) 15 | } 16 | 17 | // setup configures a new Headers middleware instance. 18 | func setup(c *caddy.Controller) error { 19 | rules, err := headersParse(c) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 25 | return Headers{Next: next, Rules: rules} 26 | }) 27 | 28 | return nil 29 | } 30 | 31 | func headersParse(c *caddy.Controller) ([]Rule, error) { 32 | var rules []Rule 33 | 34 | for c.NextLine() { 35 | var head Rule 36 | head.Headers = http.Header{} 37 | var isNewPattern bool 38 | 39 | if !c.NextArg() { 40 | return rules, c.ArgErr() 41 | } 42 | pattern := c.Val() 43 | 44 | // See if we already have a definition for this Path pattern... 45 | for _, h := range rules { 46 | if h.Path == pattern { 47 | head = h 48 | break 49 | } 50 | } 51 | 52 | // ...otherwise, this is a new pattern 53 | if head.Path == "" { 54 | head.Path = pattern 55 | isNewPattern = true 56 | } 57 | 58 | for c.NextBlock() { 59 | // A block of headers was opened... 60 | name := c.Val() 61 | value := "" 62 | 63 | args := c.RemainingArgs() 64 | 65 | if len(args) > 1 { 66 | return rules, c.ArgErr() 67 | } else if len(args) == 1 { 68 | value = args[0] 69 | } 70 | 71 | head.Headers.Add(name, value) 72 | } 73 | if c.NextArg() { 74 | // ... or single header was defined as an argument instead. 75 | 76 | name := c.Val() 77 | value := c.Val() 78 | 79 | if c.NextArg() { 80 | value = c.Val() 81 | } 82 | 83 | head.Headers.Add(name, value) 84 | } 85 | 86 | if isNewPattern { 87 | rules = append(rules, head) 88 | } else { 89 | for i := 0; i < len(rules); i++ { 90 | if rules[i].Path == pattern { 91 | rules[i] = head 92 | break 93 | } 94 | } 95 | } 96 | } 97 | 98 | return rules, nil 99 | } 100 | -------------------------------------------------------------------------------- /caddyhttp/status/setup.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | // init registers Status plugin 11 | func init() { 12 | caddy.RegisterPlugin("status", caddy.Plugin{ 13 | ServerType: "http", 14 | Action: setup, 15 | }) 16 | } 17 | 18 | // setup configures new Status middleware instance. 19 | func setup(c *caddy.Controller) error { 20 | rules, err := statusParse(c) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | cfg := httpserver.GetConfig(c) 26 | mid := func(next httpserver.Handler) httpserver.Handler { 27 | return Status{Rules: rules, Next: next} 28 | } 29 | cfg.AddMiddleware(mid) 30 | 31 | return nil 32 | } 33 | 34 | // statusParse parses status directive 35 | func statusParse(c *caddy.Controller) ([]httpserver.HandlerConfig, error) { 36 | var rules []httpserver.HandlerConfig 37 | 38 | for c.Next() { 39 | hadBlock := false 40 | args := c.RemainingArgs() 41 | 42 | switch len(args) { 43 | case 1: 44 | status, err := strconv.Atoi(args[0]) 45 | if err != nil { 46 | return rules, c.Errf("Expecting a numeric status code, got '%s'", args[0]) 47 | } 48 | 49 | for c.NextBlock() { 50 | hadBlock = true 51 | basePath := c.Val() 52 | 53 | for _, cfg := range rules { 54 | rule := cfg.(*Rule) 55 | if rule.Base == basePath { 56 | return rules, c.Errf("Duplicate path: '%s'", basePath) 57 | } 58 | } 59 | 60 | rule := NewRule(basePath, status) 61 | rules = append(rules, rule) 62 | 63 | if c.NextArg() { 64 | return rules, c.ArgErr() 65 | } 66 | } 67 | 68 | if !hadBlock { 69 | return rules, c.ArgErr() 70 | } 71 | case 2: 72 | status, err := strconv.Atoi(args[0]) 73 | if err != nil { 74 | return rules, c.Errf("Expecting a numeric status code, got '%s'", args[0]) 75 | } 76 | 77 | basePath := args[1] 78 | for _, cfg := range rules { 79 | rule := cfg.(*Rule) 80 | if rule.Base == basePath { 81 | return rules, c.Errf("Duplicate path: '%s'", basePath) 82 | } 83 | } 84 | 85 | rule := NewRule(basePath, status) 86 | rules = append(rules, rule) 87 | default: 88 | return rules, c.ArgErr() 89 | } 90 | } 91 | 92 | return rules, nil 93 | } 94 | -------------------------------------------------------------------------------- /caddyhttp/status/status_test.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | func TestStatus(t *testing.T) { 13 | status := Status{ 14 | Rules: []httpserver.HandlerConfig{ 15 | NewRule("/foo", http.StatusNotFound), 16 | NewRule("/teapot", http.StatusTeapot), 17 | NewRule("/foo/bar1", http.StatusInternalServerError), 18 | NewRule("/temporary-redirected", http.StatusTemporaryRedirect), 19 | }, 20 | Next: httpserver.HandlerFunc(urlPrinter), 21 | } 22 | 23 | tests := []struct { 24 | path string 25 | statusExpected bool 26 | status int 27 | }{ 28 | {"/foo", true, http.StatusNotFound}, 29 | {"/teapot", true, http.StatusTeapot}, 30 | {"/foo/bar", true, http.StatusNotFound}, 31 | {"/foo/bar1", true, http.StatusInternalServerError}, 32 | {"/someotherpath", false, 0}, 33 | {"/temporary-redirected", false, http.StatusTemporaryRedirect}, 34 | } 35 | 36 | for i, test := range tests { 37 | req, err := http.NewRequest("GET", test.path, nil) 38 | if err != nil { 39 | t.Fatalf("Test %d: Could not create HTTP request: %v", 40 | i, err) 41 | } 42 | 43 | rec := httptest.NewRecorder() 44 | actualStatus, err := status.ServeHTTP(rec, req) 45 | if err != nil { 46 | t.Fatalf("Test %d: Serving request failed with error %v", 47 | i, err) 48 | } 49 | 50 | if test.statusExpected { 51 | if test.status != actualStatus { 52 | t.Errorf("Test %d: Expected status code %d, got %d", 53 | i, test.status, actualStatus) 54 | } 55 | if rec.Body.String() != "" { 56 | t.Errorf("Test %d: Expected empty body, got '%s'", 57 | i, rec.Body.String()) 58 | } 59 | } else { 60 | if test.status != 0 { // Expecting status in response 61 | if test.status != rec.Code { 62 | t.Errorf("Test %d: Expected status code %d, got %d", 63 | i, test.status, rec.Code) 64 | } 65 | } else if rec.Body.String() != test.path { 66 | t.Errorf("Test %d: Expected body '%s', got '%s'", 67 | i, test.path, rec.Body.String()) 68 | } 69 | } 70 | } 71 | } 72 | 73 | func urlPrinter(w http.ResponseWriter, r *http.Request) (int, error) { 74 | fmt.Fprint(w, r.URL.String()) 75 | return 0, nil 76 | } 77 | -------------------------------------------------------------------------------- /caddyhttp/fastcgi/fcgi_test.php: -------------------------------------------------------------------------------- 1 | $val) { 16 | $md5 = md5($val); 17 | 18 | if ($key != $md5) { 19 | $stat = "FAILED"; 20 | echo "server:err ".$md5." != ".$key."\n"; 21 | } 22 | 23 | $length += strlen($key) + strlen($val); 24 | 25 | $ret .= $key."(".strlen($key).") "; 26 | } 27 | $ret .= "] ["; 28 | foreach ($_FILES as $k0 => $val) { 29 | 30 | $error = $val["error"]; 31 | if ($error == UPLOAD_ERR_OK) { 32 | $tmp_name = $val["tmp_name"]; 33 | $name = $val["name"]; 34 | $datafile = "/tmp/test.go"; 35 | move_uploaded_file($tmp_name, $datafile); 36 | $md5 = md5_file($datafile); 37 | 38 | if ($k0 != $md5) { 39 | $stat = "FAILED"; 40 | echo "server:err ".$md5." != ".$key."\n"; 41 | } 42 | 43 | $length += strlen($k0) + filesize($datafile); 44 | 45 | unlink($datafile); 46 | $ret .= $k0."(".strlen($k0).") "; 47 | } 48 | else{ 49 | $stat = "FAILED"; 50 | echo "server:file err ".file_upload_error_message($error)."\n"; 51 | } 52 | } 53 | $ret .= "]"; 54 | echo "server:got data length " .$length."\n"; 55 | } 56 | 57 | 58 | echo "-{$stat}-POST(".count($_POST).") FILE(".count($_FILES).")\n"; 59 | 60 | function file_upload_error_message($error_code) { 61 | switch ($error_code) { 62 | case UPLOAD_ERR_INI_SIZE: 63 | return 'The uploaded file exceeds the upload_max_filesize directive in php.ini'; 64 | case UPLOAD_ERR_FORM_SIZE: 65 | return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'; 66 | case UPLOAD_ERR_PARTIAL: 67 | return 'The uploaded file was only partially uploaded'; 68 | case UPLOAD_ERR_NO_FILE: 69 | return 'No file was uploaded'; 70 | case UPLOAD_ERR_NO_TMP_DIR: 71 | return 'Missing a temporary folder'; 72 | case UPLOAD_ERR_CANT_WRITE: 73 | return 'Failed to write file to disk'; 74 | case UPLOAD_ERR_EXTENSION: 75 | return 'File upload stopped by extension'; 76 | default: 77 | return 'Unknown upload error'; 78 | } 79 | } -------------------------------------------------------------------------------- /caddytls/httphandler_test.go: -------------------------------------------------------------------------------- 1 | package caddytls 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHTTPChallengeHandlerNoOp(t *testing.T) { 11 | namesObtaining.Add([]string{"localhost"}) 12 | 13 | // try base paths and host names that aren't 14 | // handled by this handler 15 | for _, url := range []string{ 16 | "http://localhost/", 17 | "http://localhost/foo.html", 18 | "http://localhost/.git", 19 | "http://localhost/.well-known/", 20 | "http://localhost/.well-known/acme-challenging", 21 | "http://other/.well-known/acme-challenge/foo", 22 | } { 23 | req, err := http.NewRequest("GET", url, nil) 24 | if err != nil { 25 | t.Fatalf("Could not craft request, got error: %v", err) 26 | } 27 | rw := httptest.NewRecorder() 28 | if HTTPChallengeHandler(rw, req, "", DefaultHTTPAlternatePort) { 29 | t.Errorf("Got true with this URL, but shouldn't have: %s", url) 30 | } 31 | } 32 | } 33 | 34 | func TestHTTPChallengeHandlerSuccess(t *testing.T) { 35 | expectedPath := challengeBasePath + "/asdf" 36 | 37 | // Set up fake acme handler backend to make sure proxying succeeds 38 | var proxySuccess bool 39 | ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | proxySuccess = true 41 | if r.URL.Path != expectedPath { 42 | t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path) 43 | } 44 | })) 45 | 46 | // Custom listener that uses the port we expect 47 | ln, err := net.Listen("tcp", "127.0.0.1:"+DefaultHTTPAlternatePort) 48 | if err != nil { 49 | t.Fatalf("Unable to start test server listener: %v", err) 50 | } 51 | ts.Listener = ln 52 | 53 | // Tell this package that we are handling a challenge for 127.0.0.1 54 | namesObtaining.Add([]string{"127.0.0.1"}) 55 | 56 | // Start our engines and run the test 57 | ts.Start() 58 | defer ts.Close() 59 | req, err := http.NewRequest("GET", "http://127.0.0.1:"+DefaultHTTPAlternatePort+expectedPath, nil) 60 | if err != nil { 61 | t.Fatalf("Could not craft request, got error: %v", err) 62 | } 63 | rw := httptest.NewRecorder() 64 | 65 | HTTPChallengeHandler(rw, req, "", DefaultHTTPAlternatePort) 66 | 67 | if !proxySuccess { 68 | t.Fatal("Expected request to be proxied, but it wasn't") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /caddyhttp/extensions/setup_test.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | c := caddy.NewTestController("http", `ext .html .htm .php`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Fatalf("Expected no errors, got: %v", err) 15 | } 16 | 17 | mids := httpserver.GetConfig(c).Middleware() 18 | if len(mids) == 0 { 19 | t.Fatal("Expected middleware, had 0 instead") 20 | } 21 | 22 | handler := mids[0](httpserver.EmptyNext) 23 | myHandler, ok := handler.(Ext) 24 | 25 | if !ok { 26 | t.Fatalf("Expected handler to be type Ext, got: %#v", handler) 27 | } 28 | 29 | if myHandler.Extensions[0] != ".html" { 30 | t.Errorf("Expected .html in the list of Extensions") 31 | } 32 | if myHandler.Extensions[1] != ".htm" { 33 | t.Errorf("Expected .htm in the list of Extensions") 34 | } 35 | if myHandler.Extensions[2] != ".php" { 36 | t.Errorf("Expected .php in the list of Extensions") 37 | } 38 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 39 | t.Error("'Next' field of handler was not set properly") 40 | } 41 | 42 | } 43 | 44 | func TestExtParse(t *testing.T) { 45 | tests := []struct { 46 | inputExts string 47 | shouldErr bool 48 | expectedExts []string 49 | }{ 50 | {`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}}, 51 | {`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}}, 52 | {`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}}, 53 | } 54 | for i, test := range tests { 55 | actualExts, err := extParse(caddy.NewTestController("http", test.inputExts)) 56 | 57 | if err == nil && test.shouldErr { 58 | t.Errorf("Test %d didn't error, but it should have", i) 59 | } else if err != nil && !test.shouldErr { 60 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 61 | } 62 | 63 | if len(actualExts) != len(test.expectedExts) { 64 | t.Fatalf("Test %d expected %d rules, but got %d", 65 | i, len(test.expectedExts), len(actualExts)) 66 | } 67 | for j, actualExt := range actualExts { 68 | if actualExt != test.expectedExts[j] { 69 | t.Fatalf("Test %d expected %dth extension to be %s , but got %s", 70 | i, j, test.expectedExts[j], actualExt) 71 | } 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /caddyhttp/rewrite/to.go: -------------------------------------------------------------------------------- 1 | package rewrite 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "path" 9 | "strings" 10 | 11 | "github.com/mholt/caddy/caddyhttp/httpserver" 12 | ) 13 | 14 | // To attempts rewrite. It attempts to rewrite to first valid path 15 | // or the last path if none of the paths are valid. 16 | func To(fs http.FileSystem, r *http.Request, to string, replacer httpserver.Replacer) Result { 17 | tos := strings.Fields(to) 18 | 19 | // try each rewrite paths 20 | t := "" 21 | query := "" 22 | for _, v := range tos { 23 | t = replacer.Replace(v) 24 | tparts := strings.SplitN(t, "?", 2) 25 | t = path.Clean(tparts[0]) 26 | 27 | if len(tparts) > 1 { 28 | query = tparts[1] 29 | } 30 | 31 | // add trailing slash for directories, if present 32 | if strings.HasSuffix(tparts[0], "/") && !strings.HasSuffix(t, "/") { 33 | t += "/" 34 | } 35 | 36 | // validate file 37 | if validFile(fs, t) { 38 | break 39 | } 40 | } 41 | 42 | // validate resulting path 43 | u, err := url.Parse(t) 44 | if err != nil { 45 | // Let the user know we got here. Rewrite is expected but 46 | // the resulting url is invalid. 47 | log.Printf("[ERROR] rewrite: resulting path '%v' is invalid. error: %v", t, err) 48 | return RewriteIgnored 49 | } 50 | 51 | // take note of this rewrite for internal use by fastcgi 52 | // all we need is the URI, not full URL 53 | *r = *r.WithContext(context.WithValue(r.Context(), httpserver.URIxRewriteCtxKey, r.URL.RequestURI())) 54 | 55 | // perform rewrite 56 | r.URL.Path = u.Path 57 | if query != "" { 58 | // overwrite query string if present 59 | r.URL.RawQuery = query 60 | } 61 | if u.Fragment != "" { 62 | // overwrite fragment if present 63 | r.URL.Fragment = u.Fragment 64 | } 65 | 66 | return RewriteDone 67 | } 68 | 69 | // validFile checks if file exists on the filesystem. 70 | // if file ends with `/`, it is validated as a directory. 71 | func validFile(fs http.FileSystem, file string) bool { 72 | if fs == nil { 73 | return false 74 | } 75 | 76 | f, err := fs.Open(file) 77 | if err != nil { 78 | return false 79 | } 80 | defer f.Close() 81 | 82 | stat, err := f.Stat() 83 | if err != nil { 84 | return false 85 | } 86 | 87 | // directory 88 | if strings.HasSuffix(file, "/") { 89 | return stat.IsDir() 90 | } 91 | 92 | // file 93 | return !stat.IsDir() 94 | } 95 | -------------------------------------------------------------------------------- /caddyhttp/websocket/setup.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | func init() { 9 | caddy.RegisterPlugin("websocket", caddy.Plugin{ 10 | ServerType: "http", 11 | Action: setup, 12 | }) 13 | } 14 | 15 | // setup configures a new WebSocket middleware instance. 16 | func setup(c *caddy.Controller) error { 17 | websocks, err := webSocketParse(c) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | GatewayInterface = caddy.AppName + "-CGI/1.1" 23 | ServerSoftware = caddy.AppName + "/" + caddy.AppVersion 24 | 25 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 26 | return WebSocket{Next: next, Sockets: websocks} 27 | }) 28 | 29 | return nil 30 | } 31 | 32 | func webSocketParse(c *caddy.Controller) ([]Config, error) { 33 | var websocks []Config 34 | var respawn bool 35 | 36 | optionalBlock := func() (hadBlock bool, err error) { 37 | for c.NextBlock() { 38 | hadBlock = true 39 | if c.Val() == "respawn" { 40 | respawn = true 41 | } else { 42 | return true, c.Err("Expected websocket configuration parameter in block") 43 | } 44 | } 45 | return 46 | } 47 | 48 | for c.Next() { 49 | var val, path, command string 50 | 51 | // Path or command; not sure which yet 52 | if !c.NextArg() { 53 | return nil, c.ArgErr() 54 | } 55 | val = c.Val() 56 | 57 | // Extra configuration may be in a block 58 | hadBlock, err := optionalBlock() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if !hadBlock { 64 | // The next argument on this line will be the command or an open curly brace 65 | if c.NextArg() { 66 | path = val 67 | command = c.Val() 68 | } else { 69 | path = "/" 70 | command = val 71 | } 72 | 73 | // Okay, check again for optional block 74 | _, err = optionalBlock() 75 | if err != nil { 76 | return nil, err 77 | } 78 | } 79 | 80 | // Split command into the actual command and its arguments 81 | cmd, args, err := caddy.SplitCommandAndArgs(command) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | websocks = append(websocks, Config{ 87 | Path: path, 88 | Command: cmd, 89 | Arguments: args, 90 | Respawn: respawn, // TODO: This isn't used currently 91 | }) 92 | } 93 | 94 | return websocks, nil 95 | 96 | } 97 | -------------------------------------------------------------------------------- /caddyhttp/templates/setup.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func init() { 11 | caddy.RegisterPlugin("templates", caddy.Plugin{ 12 | ServerType: "http", 13 | Action: setup, 14 | }) 15 | } 16 | 17 | // setup configures a new Templates middleware instance. 18 | func setup(c *caddy.Controller) error { 19 | rules, err := templatesParse(c) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | cfg := httpserver.GetConfig(c) 25 | 26 | tmpls := Templates{ 27 | Rules: rules, 28 | Root: cfg.Root, 29 | FileSys: http.Dir(cfg.Root), 30 | } 31 | 32 | cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 33 | tmpls.Next = next 34 | return tmpls 35 | }) 36 | 37 | return nil 38 | } 39 | 40 | func templatesParse(c *caddy.Controller) ([]Rule, error) { 41 | var rules []Rule 42 | 43 | for c.Next() { 44 | var rule Rule 45 | 46 | rule.Path = defaultTemplatePath 47 | rule.Extensions = defaultTemplateExtensions 48 | 49 | args := c.RemainingArgs() 50 | 51 | switch len(args) { 52 | case 0: 53 | // Optional block 54 | for c.NextBlock() { 55 | switch c.Val() { 56 | case "path": 57 | args := c.RemainingArgs() 58 | if len(args) != 1 { 59 | return nil, c.ArgErr() 60 | } 61 | rule.Path = args[0] 62 | 63 | case "ext": 64 | args := c.RemainingArgs() 65 | if len(args) == 0 { 66 | return nil, c.ArgErr() 67 | } 68 | rule.Extensions = args 69 | 70 | case "between": 71 | args := c.RemainingArgs() 72 | if len(args) != 2 { 73 | return nil, c.ArgErr() 74 | } 75 | rule.Delims[0] = args[0] 76 | rule.Delims[1] = args[1] 77 | } 78 | } 79 | default: 80 | // First argument would be the path 81 | rule.Path = args[0] 82 | 83 | // Any remaining arguments are extensions 84 | rule.Extensions = args[1:] 85 | if len(rule.Extensions) == 0 { 86 | rule.Extensions = defaultTemplateExtensions 87 | } 88 | } 89 | 90 | for _, ext := range rule.Extensions { 91 | rule.IndexFiles = append(rule.IndexFiles, "index"+ext) 92 | } 93 | 94 | rules = append(rules, rule) 95 | } 96 | return rules, nil 97 | } 98 | 99 | const defaultTemplatePath = "/" 100 | 101 | var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"} 102 | -------------------------------------------------------------------------------- /dist/init/linux-sysvinit/caddy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: caddy 4 | # Required-Start: $local_fs $network $named $time $syslog 5 | # Required-Stop: $local_fs $network $named $time $syslog 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: starts the caddy web server 9 | # Description: starts caddy using start-stop-daemon 10 | ### END INIT INFO 11 | 12 | # Original Author: Frédéric Galusik (fredg) 13 | # Maintainer: Daniel van Dorp (djvdorp) 14 | 15 | DESC="the caddy web server" 16 | NAME=caddy 17 | DAEMON=$(which caddy) 18 | 19 | DAEMONUSER=www-data 20 | PIDFILE=/var/run/$NAME.pid 21 | LOGFILE=/var/log/$NAME.log 22 | CONFIGFILE=/etc/caddy/Caddyfile 23 | DAEMONOPTS="-agree=true -pidfile=$PIDFILE -log=$LOGFILE -conf=$CONFIGFILE" 24 | 25 | USERBIND="setcap cap_net_bind_service=+ep" 26 | STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}" 27 | 28 | test -x $DAEMON || exit 0 29 | 30 | # Set the CADDYPATH; Let's Encrypt certificates will be written to this directory. 31 | export CADDYPATH=/etc/ssl/caddy 32 | 33 | # Set the ulimits 34 | ulimit -n 8192 35 | 36 | 37 | start() { 38 | $USERBIND $DAEMON 39 | start-stop-daemon --start --quiet --make-pidfile --pidfile $PIDFILE \ 40 | --background --chuid $DAEMONUSER --oknodo --exec $DAEMON -- $DAEMONOPTS 41 | } 42 | 43 | stop() { 44 | start-stop-daemon --stop --quiet --pidfile $PIDFILE --retry=$STOP_SCHEDULE \ 45 | --name $NAME --oknodo 46 | rm -f $PIDFILE 47 | } 48 | 49 | reload() { 50 | start-stop-daemon --stop --quiet --signal USR1 --pidfile $PIDFILE \ 51 | --name $NAME 52 | } 53 | 54 | status() { 55 | if [ -f $PIDFILE ]; then 56 | if kill -0 $(cat "$PIDFILE"); then 57 | echo "$NAME is running" 58 | else 59 | echo "$NAME process is dead, but pidfile exists" 60 | fi 61 | else 62 | echo "$NAME is not running" 63 | fi 64 | } 65 | 66 | case "$1" in 67 | start) 68 | echo "Starting $NAME" 69 | start 70 | ;; 71 | stop) 72 | echo "Stopping $NAME" 73 | stop 74 | ;; 75 | restart) 76 | echo "Restarting $NAME" 77 | stop 78 | start 79 | ;; 80 | reload) 81 | echo "Reloading $NAME configuration" 82 | reload 83 | ;; 84 | status) 85 | status 86 | ;; 87 | *) 88 | echo "Usage: $0 {start|stop|restart|reload|status}" 89 | exit 2 90 | ;; 91 | esac 92 | 93 | exit 0 94 | -------------------------------------------------------------------------------- /caddyhttp/fastcgi/dialer.go: -------------------------------------------------------------------------------- 1 | package fastcgi 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | type dialer interface { 11 | Dial() (Client, error) 12 | Close(Client) error 13 | } 14 | 15 | // basicDialer is a basic dialer that wraps default fcgi functions. 16 | type basicDialer struct { 17 | network string 18 | address string 19 | timeout time.Duration 20 | } 21 | 22 | func (b basicDialer) Dial() (Client, error) { 23 | return DialTimeout(b.network, b.address, b.timeout) 24 | } 25 | 26 | func (b basicDialer) Close(c Client) error { return c.Close() } 27 | 28 | // persistentDialer keeps a pool of fcgi connections. 29 | // connections are not closed after use, rather added back to the pool for reuse. 30 | type persistentDialer struct { 31 | size int 32 | network string 33 | address string 34 | timeout time.Duration 35 | pool []Client 36 | sync.Mutex 37 | } 38 | 39 | func (p *persistentDialer) Dial() (Client, error) { 40 | p.Lock() 41 | // connection is available, return first one. 42 | if len(p.pool) > 0 { 43 | client := p.pool[0] 44 | p.pool = p.pool[1:] 45 | p.Unlock() 46 | 47 | return client, nil 48 | } 49 | 50 | p.Unlock() 51 | 52 | // no connection available, create new one 53 | return DialTimeout(p.network, p.address, p.timeout) 54 | } 55 | 56 | func (p *persistentDialer) Close(client Client) error { 57 | p.Lock() 58 | if len(p.pool) < p.size { 59 | // pool is not full yet, add connection for reuse 60 | p.pool = append(p.pool, client) 61 | p.Unlock() 62 | 63 | return nil 64 | } 65 | 66 | p.Unlock() 67 | 68 | // otherwise, close the connection. 69 | return client.Close() 70 | } 71 | 72 | type loadBalancingDialer struct { 73 | current int64 74 | dialers []dialer 75 | } 76 | 77 | func (m *loadBalancingDialer) Dial() (Client, error) { 78 | nextDialerIndex := atomic.AddInt64(&m.current, 1) % int64(len(m.dialers)) 79 | currentDialer := m.dialers[nextDialerIndex] 80 | 81 | client, err := currentDialer.Dial() 82 | 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return &dialerAwareClient{Client: client, dialer: currentDialer}, nil 88 | } 89 | 90 | func (m *loadBalancingDialer) Close(c Client) error { 91 | // Close the client according to dialer behaviour 92 | if da, ok := c.(*dialerAwareClient); ok { 93 | return da.dialer.Close(c) 94 | } 95 | 96 | return errors.New("Cannot close client") 97 | } 98 | 99 | type dialerAwareClient struct { 100 | Client 101 | dialer dialer 102 | } 103 | -------------------------------------------------------------------------------- /caddy_test.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | /* 10 | // TODO 11 | func TestCaddyStartStop(t *testing.T) { 12 | caddyfile := "localhost:1984" 13 | 14 | for i := 0; i < 2; i++ { 15 | _, err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) 16 | if err != nil { 17 | t.Fatalf("Error starting, iteration %d: %v", i, err) 18 | } 19 | 20 | client := http.Client{ 21 | Timeout: time.Duration(2 * time.Second), 22 | } 23 | resp, err := client.Get("http://localhost:1984") 24 | if err != nil { 25 | t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) 26 | } 27 | resp.Body.Close() 28 | 29 | err = Stop() 30 | if err != nil { 31 | t.Fatalf("Error stopping, iteration %d: %v", i, err) 32 | } 33 | } 34 | } 35 | */ 36 | 37 | func TestIsLoopback(t *testing.T) { 38 | for i, test := range []struct { 39 | input string 40 | expect bool 41 | }{ 42 | {"example.com", false}, 43 | {"localhost", true}, 44 | {"localhost:1234", true}, 45 | {"localhost:", true}, 46 | {"127.0.0.1", true}, 47 | {"127.0.0.1:443", true}, 48 | {"127.0.1.5", true}, 49 | {"10.0.0.5", false}, 50 | {"12.7.0.1", false}, 51 | {"[::1]", true}, 52 | {"[::1]:1234", true}, 53 | {"::1", true}, 54 | {"::", false}, 55 | {"[::]", false}, 56 | {"local", false}, 57 | } { 58 | if got, want := IsLoopback(test.input), test.expect; got != want { 59 | t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got) 60 | } 61 | } 62 | } 63 | 64 | func TestListenerAddrEqual(t *testing.T) { 65 | ln1, err := net.Listen("tcp", "[::]:0") 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | defer ln1.Close() 70 | ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port) 71 | 72 | ln2, err := net.Listen("tcp", "127.0.0.1:0") 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | defer ln2.Close() 77 | ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port) 78 | 79 | for i, test := range []struct { 80 | ln net.Listener 81 | addr string 82 | expect bool 83 | }{ 84 | {ln1, ":1234", false}, 85 | {ln1, "0.0.0.0:1234", false}, 86 | {ln1, "0.0.0.0", false}, 87 | {ln1, ":" + ln1port, true}, 88 | {ln1, "0.0.0.0:" + ln1port, true}, 89 | {ln2, ":" + ln2port, false}, 90 | {ln2, "127.0.0.1:1234", false}, 91 | {ln2, "127.0.0.1", false}, 92 | {ln2, "127.0.0.1:" + ln2port, true}, 93 | } { 94 | if got, want := listenerAddrEqual(test.ln, test.addr), test.expect; got != want { 95 | t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sigtrap_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package caddy 4 | 5 | import ( 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | // trapSignalsPosix captures POSIX-only signals. 13 | func trapSignalsPosix() { 14 | go func() { 15 | sigchan := make(chan os.Signal, 1) 16 | signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1) 17 | 18 | for sig := range sigchan { 19 | switch sig { 20 | case syscall.SIGTERM: 21 | log.Println("[INFO] SIGTERM: Terminating process") 22 | if PidFile != "" { 23 | os.Remove(PidFile) 24 | } 25 | os.Exit(0) 26 | 27 | case syscall.SIGQUIT: 28 | log.Println("[INFO] SIGQUIT: Shutting down") 29 | exitCode := executeShutdownCallbacks("SIGQUIT") 30 | err := Stop() 31 | if err != nil { 32 | log.Printf("[ERROR] SIGQUIT stop: %v", err) 33 | exitCode = 1 34 | } 35 | if PidFile != "" { 36 | os.Remove(PidFile) 37 | } 38 | os.Exit(exitCode) 39 | 40 | case syscall.SIGHUP: 41 | log.Println("[INFO] SIGHUP: Hanging up") 42 | err := Stop() 43 | if err != nil { 44 | log.Printf("[ERROR] SIGHUP stop: %v", err) 45 | } 46 | 47 | case syscall.SIGUSR1: 48 | log.Println("[INFO] SIGUSR1: Reloading") 49 | 50 | // Start with the existing Caddyfile 51 | instancesMu.Lock() 52 | if len(instances) == 0 { 53 | instancesMu.Unlock() 54 | log.Println("[ERROR] SIGUSR1: No server instances are fully running") 55 | continue 56 | } 57 | inst := instances[0] // we only support one instance at this time 58 | instancesMu.Unlock() 59 | 60 | updatedCaddyfile := inst.caddyfileInput 61 | if updatedCaddyfile == nil { 62 | // Hmm, did spawing process forget to close stdin? Anyhow, this is unusual. 63 | log.Println("[ERROR] SIGUSR1: no Caddyfile to reload (was stdin left open?)") 64 | continue 65 | } 66 | if loaderUsed.loader == nil { 67 | // This also should never happen 68 | log.Println("[ERROR] SIGUSR1: no Caddyfile loader with which to reload Caddyfile") 69 | continue 70 | } 71 | 72 | // Load the updated Caddyfile 73 | newCaddyfile, err := loaderUsed.loader.Load(inst.serverType) 74 | if err != nil { 75 | log.Printf("[ERROR] SIGUSR1: loading updated Caddyfile: %v", err) 76 | continue 77 | } 78 | if newCaddyfile != nil { 79 | updatedCaddyfile = newCaddyfile 80 | } 81 | 82 | // Kick off the restart; our work is done 83 | inst, err = inst.Restart(updatedCaddyfile) 84 | if err != nil { 85 | log.Printf("[ERROR] SIGUSR1: %v", err) 86 | } 87 | } 88 | } 89 | }() 90 | } 91 | -------------------------------------------------------------------------------- /caddyhttp/rewrite/setup.go: -------------------------------------------------------------------------------- 1 | package rewrite 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/mholt/caddy" 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | ) 10 | 11 | func init() { 12 | caddy.RegisterPlugin("rewrite", caddy.Plugin{ 13 | ServerType: "http", 14 | Action: setup, 15 | }) 16 | } 17 | 18 | // setup configures a new Rewrite middleware instance. 19 | func setup(c *caddy.Controller) error { 20 | rewrites, err := rewriteParse(c) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | cfg := httpserver.GetConfig(c) 26 | 27 | cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 28 | return Rewrite{ 29 | Next: next, 30 | FileSys: http.Dir(cfg.Root), 31 | Rules: rewrites, 32 | } 33 | }) 34 | 35 | return nil 36 | } 37 | 38 | func rewriteParse(c *caddy.Controller) ([]httpserver.HandlerConfig, error) { 39 | var rules []httpserver.HandlerConfig 40 | 41 | for c.Next() { 42 | var rule Rule 43 | var err error 44 | var base = "/" 45 | var pattern, to string 46 | var ext []string 47 | 48 | args := c.RemainingArgs() 49 | 50 | var matcher httpserver.RequestMatcher 51 | 52 | switch len(args) { 53 | case 1: 54 | base = args[0] 55 | fallthrough 56 | case 0: 57 | // Integrate request matcher for 'if' conditions. 58 | matcher, err = httpserver.SetupIfMatcher(c) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | for c.NextBlock() { 64 | if httpserver.IfMatcherKeyword(c) { 65 | continue 66 | } 67 | switch c.Val() { 68 | case "r", "regexp": 69 | if !c.NextArg() { 70 | return nil, c.ArgErr() 71 | } 72 | pattern = c.Val() 73 | case "to": 74 | args1 := c.RemainingArgs() 75 | if len(args1) == 0 { 76 | return nil, c.ArgErr() 77 | } 78 | to = strings.Join(args1, " ") 79 | case "ext": 80 | args1 := c.RemainingArgs() 81 | if len(args1) == 0 { 82 | return nil, c.ArgErr() 83 | } 84 | ext = args1 85 | default: 86 | return nil, c.ArgErr() 87 | } 88 | } 89 | // ensure to is specified 90 | if to == "" { 91 | return nil, c.ArgErr() 92 | } 93 | if rule, err = NewComplexRule(base, pattern, to, ext, matcher); err != nil { 94 | return nil, err 95 | } 96 | rules = append(rules, rule) 97 | 98 | // the only unhandled case is 2 and above 99 | default: 100 | rule = NewSimpleRule(args[0], strings.Join(args[1:], " ")) 101 | rules = append(rules, rule) 102 | } 103 | 104 | } 105 | 106 | return rules, nil 107 | } 108 | -------------------------------------------------------------------------------- /caddyhttp/markdown/template.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "text/template" 7 | 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | "github.com/mholt/caddy/caddyhttp/markdown/metadata" 10 | ) 11 | 12 | // Data represents a markdown document. 13 | type Data struct { 14 | httpserver.Context 15 | Doc map[string]interface{} 16 | Styles []string 17 | Scripts []string 18 | Meta map[string]string 19 | Files []FileInfo 20 | } 21 | 22 | // Include "overrides" the embedded httpserver.Context's Include() 23 | // method so that included files have access to d's fields. 24 | // Note: using {{template 'template-name' .}} instead might be better. 25 | func (d Data) Include(filename string) (string, error) { 26 | return httpserver.ContextInclude(filename, d, d.Root) 27 | } 28 | 29 | // execTemplate executes a template given a requestPath, template, and metadata 30 | func execTemplate(c *Config, mdata metadata.Metadata, meta map[string]string, files []FileInfo, ctx httpserver.Context) ([]byte, error) { 31 | mdData := Data{ 32 | Context: ctx, 33 | Doc: mdata.Variables, 34 | Styles: c.Styles, 35 | Scripts: c.Scripts, 36 | Meta: meta, 37 | Files: files, 38 | } 39 | 40 | b := new(bytes.Buffer) 41 | if err := c.Template.ExecuteTemplate(b, mdata.Template, mdData); err != nil { 42 | return nil, err 43 | } 44 | 45 | return b.Bytes(), nil 46 | } 47 | 48 | // SetTemplate reads in the template with the filename provided. If the file does not exist or is not parsable, it will return an error. 49 | func SetTemplate(t *template.Template, name, filename string) error { 50 | 51 | // Read template 52 | buf, err := ioutil.ReadFile(filename) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // Update if exists 58 | if tt := t.Lookup(name); tt != nil { 59 | _, err = tt.Parse(string(buf)) 60 | return err 61 | } 62 | 63 | // Allocate new name if not 64 | _, err = t.New(name).Parse(string(buf)) 65 | return err 66 | } 67 | 68 | // GetDefaultTemplate returns the default template. 69 | func GetDefaultTemplate() *template.Template { 70 | return template.Must(template.New("").Parse(defaultTemplate)) 71 | } 72 | 73 | const ( 74 | defaultTemplate = ` 75 | 76 | 77 | {{.Doc.title}} 78 | 79 | {{range $key, $val := .Meta}} 80 | 81 | {{end}} 82 | {{- range .Styles}} 83 | 84 | {{- end}} 85 | {{- range .Scripts}} 86 | 87 | {{- end}} 88 | 89 | 90 | {{.Doc.body}} 91 | 92 | ` 93 | ) 94 | -------------------------------------------------------------------------------- /caddyhttp/markdown/process.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/mholt/caddy/caddyhttp/httpserver" 9 | "github.com/mholt/caddy/caddyhttp/markdown/metadata" 10 | "github.com/mholt/caddy/caddyhttp/markdown/summary" 11 | "github.com/russross/blackfriday" 12 | ) 13 | 14 | // FileInfo represents a file in a particular server context. It wraps the os.FileInfo struct. 15 | type FileInfo struct { 16 | os.FileInfo 17 | ctx httpserver.Context 18 | } 19 | 20 | var recognizedMetaTags = []string{ 21 | "author", 22 | "copyright", 23 | "description", 24 | "subject", 25 | } 26 | 27 | // Summarize returns an abbreviated string representation of the markdown stored in this file. 28 | // wordcount is the number of words returned in the summary. 29 | func (f FileInfo) Summarize(wordcount int) (string, error) { 30 | fp, err := f.ctx.Root.Open(f.Name()) 31 | if err != nil { 32 | return "", err 33 | } 34 | defer fp.Close() 35 | 36 | buf, err := ioutil.ReadAll(fp) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | return string(summary.Markdown(buf, wordcount)), nil 42 | } 43 | 44 | // Markdown processes the contents of a page in b. It parses the metadata 45 | // (if any) and uses the template (if found). 46 | func (c *Config) Markdown(title string, r io.Reader, dirents []os.FileInfo, ctx httpserver.Context) ([]byte, error) { 47 | body, err := ioutil.ReadAll(r) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | parser := metadata.GetParser(body) 53 | markdown := parser.Markdown() 54 | mdata := parser.Metadata() 55 | 56 | // process markdown 57 | extns := 0 58 | extns |= blackfriday.EXTENSION_TABLES 59 | extns |= blackfriday.EXTENSION_FENCED_CODE 60 | extns |= blackfriday.EXTENSION_STRIKETHROUGH 61 | extns |= blackfriday.EXTENSION_DEFINITION_LISTS 62 | html := blackfriday.Markdown(markdown, c.Renderer, extns) 63 | 64 | // set it as body for template 65 | mdata.Variables["body"] = string(html) 66 | 67 | // fixup title 68 | mdata.Variables["title"] = mdata.Title 69 | if mdata.Variables["title"] == "" { 70 | mdata.Variables["title"] = title 71 | } 72 | 73 | // move available and valid front matters to the meta values 74 | meta := make(map[string]string) 75 | for _, val := range recognizedMetaTags { 76 | if mVal, ok := mdata.Variables[val]; ok { 77 | meta[val] = mVal.(string) 78 | } 79 | } 80 | 81 | // massage possible files 82 | files := []FileInfo{} 83 | for _, ent := range dirents { 84 | file := FileInfo{ 85 | FileInfo: ent, 86 | ctx: ctx, 87 | } 88 | files = append(files, file) 89 | } 90 | 91 | return execTemplate(c, mdata, meta, files, ctx) 92 | } 93 | -------------------------------------------------------------------------------- /caddytls/handshake_test.go: -------------------------------------------------------------------------------- 1 | package caddytls 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "testing" 7 | ) 8 | 9 | func TestGetCertificate(t *testing.T) { 10 | defer func() { certCache = make(map[string]Certificate) }() 11 | 12 | cfg := new(Config) 13 | 14 | hello := &tls.ClientHelloInfo{ServerName: "example.com"} 15 | helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"} 16 | helloNoSNI := &tls.ClientHelloInfo{} 17 | helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"} 18 | 19 | // When cache is empty 20 | if cert, err := cfg.GetCertificate(hello); err == nil { 21 | t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert) 22 | } 23 | if cert, err := cfg.GetCertificate(helloNoSNI); err == nil { 24 | t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert) 25 | } 26 | 27 | // When cache has one certificate in it (also is default) 28 | defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}} 29 | certCache[""] = defaultCert 30 | certCache["example.com"] = defaultCert 31 | if cert, err := cfg.GetCertificate(hello); err != nil { 32 | t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err) 33 | } else if cert.Leaf.DNSNames[0] != "example.com" { 34 | t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert) 35 | } 36 | if cert, err := cfg.GetCertificate(helloNoSNI); err != nil { 37 | t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err) 38 | } else if cert.Leaf.DNSNames[0] != "example.com" { 39 | t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert) 40 | } 41 | 42 | // When retrieving wildcard certificate 43 | certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}} 44 | if cert, err := cfg.GetCertificate(helloSub); err != nil { 45 | t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err) 46 | } else if cert.Leaf.DNSNames[0] != "*.example.com" { 47 | t.Errorf("Got wrong certificate, expected wildcard: %v", cert) 48 | } 49 | 50 | // When no certificate matches, the default is returned 51 | if cert, err := cfg.GetCertificate(helloNoMatch); err != nil { 52 | t.Errorf("Expected default certificate with no error when no matches, got err: %v", err) 53 | } else if cert.Leaf.DNSNames[0] != "example.com" { 54 | t.Errorf("Expected default cert with no matches, got: %v", cert) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /caddyhttp/basicauth/setup.go: -------------------------------------------------------------------------------- 1 | package basicauth 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func init() { 11 | caddy.RegisterPlugin("basicauth", caddy.Plugin{ 12 | ServerType: "http", 13 | Action: setup, 14 | }) 15 | } 16 | 17 | // setup configures a new BasicAuth middleware instance. 18 | func setup(c *caddy.Controller) error { 19 | cfg := httpserver.GetConfig(c) 20 | root := cfg.Root 21 | 22 | rules, err := basicAuthParse(c) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | basic := BasicAuth{Rules: rules} 28 | 29 | cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 30 | basic.Next = next 31 | basic.SiteRoot = root 32 | return basic 33 | }) 34 | 35 | return nil 36 | } 37 | 38 | func basicAuthParse(c *caddy.Controller) ([]Rule, error) { 39 | var rules []Rule 40 | cfg := httpserver.GetConfig(c) 41 | 42 | var err error 43 | for c.Next() { 44 | var rule Rule 45 | 46 | args := c.RemainingArgs() 47 | 48 | switch len(args) { 49 | case 2: 50 | rule.Username = args[0] 51 | if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil { 52 | return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) 53 | } 54 | case 3: 55 | rule.Resources = append(rule.Resources, args[0]) 56 | rule.Username = args[1] 57 | if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil { 58 | return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) 59 | } 60 | default: 61 | return rules, c.ArgErr() 62 | } 63 | 64 | // If nested block is present, process it here 65 | for c.NextBlock() { 66 | val := c.Val() 67 | args = c.RemainingArgs() 68 | switch len(args) { 69 | case 0: 70 | // Assume single argument is path resource 71 | rule.Resources = append(rule.Resources, val) 72 | case 1: 73 | if val == "realm" { 74 | if rule.Realm == "" { 75 | rule.Realm = strings.Replace(args[0], `"`, `\"`, -1) 76 | } else { 77 | return rules, c.Errf("\"realm\" subdirective can only be specified once") 78 | } 79 | } else { 80 | return rules, c.Errf("expecting \"realm\", got \"%s\"", val) 81 | } 82 | default: 83 | return rules, c.ArgErr() 84 | } 85 | } 86 | 87 | rules = append(rules, rule) 88 | } 89 | 90 | return rules, nil 91 | } 92 | 93 | func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) { 94 | if !strings.HasPrefix(passw, "htpasswd=") { 95 | return PlainMatcher(passw), nil 96 | } 97 | return GetHtpasswdMatcher(passw[9:], username, siteRoot) 98 | } 99 | -------------------------------------------------------------------------------- /sigtrap.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | ) 9 | 10 | // TrapSignals create signal handlers for all applicable signals for this 11 | // system. If your Go program uses signals, this is a rather invasive 12 | // function; best to implement them yourself in that case. Signals are not 13 | // required for the caddy package to function properly, but this is a 14 | // convenient way to allow the user to control this part of your program. 15 | func TrapSignals() { 16 | trapSignalsCrossPlatform() 17 | trapSignalsPosix() 18 | } 19 | 20 | // trapSignalsCrossPlatform captures SIGINT, which triggers forceful 21 | // shutdown that executes shutdown callbacks first. A second interrupt 22 | // signal will exit the process immediately. 23 | func trapSignalsCrossPlatform() { 24 | go func() { 25 | shutdown := make(chan os.Signal, 1) 26 | signal.Notify(shutdown, os.Interrupt) 27 | 28 | for i := 0; true; i++ { 29 | <-shutdown 30 | 31 | if i > 0 { 32 | log.Println("[INFO] SIGINT: Force quit") 33 | if PidFile != "" { 34 | os.Remove(PidFile) 35 | } 36 | os.Exit(1) 37 | } 38 | 39 | log.Println("[INFO] SIGINT: Shutting down") 40 | 41 | if PidFile != "" { 42 | os.Remove(PidFile) 43 | } 44 | 45 | go os.Exit(executeShutdownCallbacks("SIGINT")) 46 | } 47 | }() 48 | } 49 | 50 | // executeShutdownCallbacks executes the shutdown callbacks as initiated 51 | // by signame. It logs any errors and returns the recommended exit status. 52 | // This function is idempotent; subsequent invocations always return 0. 53 | func executeShutdownCallbacks(signame string) (exitCode int) { 54 | shutdownCallbacksOnce.Do(func() { 55 | // execute third-party shutdown hooks 56 | EmitEvent(ShutdownEvent) 57 | 58 | errs := allShutdownCallbacks() 59 | if len(errs) > 0 { 60 | for _, err := range errs { 61 | log.Printf("[ERROR] %s shutdown: %v", signame, err) 62 | } 63 | exitCode = 1 64 | } 65 | }) 66 | return 67 | } 68 | 69 | // allShutdownCallbacks executes all the shutdown callbacks 70 | // for all the instances, and returns all the errors generated 71 | // during their execution. An error executing one shutdown 72 | // callback does not stop execution of others. Only one shutdown 73 | // callback is executed at a time. 74 | func allShutdownCallbacks() []error { 75 | var errs []error 76 | instancesMu.Lock() 77 | for _, inst := range instances { 78 | errs = append(errs, inst.ShutdownCallbacks()...) 79 | } 80 | instancesMu.Unlock() 81 | return errs 82 | } 83 | 84 | // shutdownCallbacksOnce ensures that shutdown callbacks 85 | // for all instances are only executed once. 86 | var shutdownCallbacksOnce sync.Once 87 | -------------------------------------------------------------------------------- /caddyhttp/header/header_test.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "reflect" 9 | "sort" 10 | "testing" 11 | 12 | "github.com/mholt/caddy/caddyhttp/httpserver" 13 | ) 14 | 15 | func TestHeader(t *testing.T) { 16 | hostname, err := os.Hostname() 17 | if err != nil { 18 | t.Fatalf("Could not determine hostname: %v", err) 19 | } 20 | for i, test := range []struct { 21 | from string 22 | name string 23 | value string 24 | }{ 25 | {"/a", "Foo", "Bar"}, 26 | {"/a", "Bar", ""}, 27 | {"/a", "Baz", ""}, 28 | {"/a", "Server", ""}, 29 | {"/a", "ServerName", hostname}, 30 | {"/b", "Foo", ""}, 31 | {"/b", "Bar", "Removed in /a"}, 32 | } { 33 | he := Headers{ 34 | Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 35 | w.Header().Set("Bar", "Removed in /a") 36 | w.WriteHeader(http.StatusOK) 37 | return 0, nil 38 | }), 39 | Rules: []Rule{ 40 | {Path: "/a", Headers: http.Header{ 41 | "Foo": []string{"Bar"}, 42 | "ServerName": []string{"{hostname}"}, 43 | "-Bar": []string{""}, 44 | "-Server": []string{}, 45 | }}, 46 | }, 47 | } 48 | 49 | req, err := http.NewRequest("GET", test.from, nil) 50 | if err != nil { 51 | t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) 52 | } 53 | 54 | rec := httptest.NewRecorder() 55 | // preset header 56 | rec.Header().Set("Server", "Caddy") 57 | 58 | he.ServeHTTP(rec, req) 59 | 60 | if got := rec.Header().Get(test.name); got != test.value { 61 | t.Errorf("Test %d: Expected %s header to be %q but was %q", 62 | i, test.name, test.value, got) 63 | } 64 | } 65 | } 66 | 67 | func TestMultipleHeaders(t *testing.T) { 68 | he := Headers{ 69 | Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 70 | fmt.Fprint(w, "This is a test") 71 | return 0, nil 72 | }), 73 | Rules: []Rule{ 74 | {Path: "/a", Headers: http.Header{ 75 | "+Link": []string{"; rel=preload", "; rel=preload"}, 76 | }}, 77 | }, 78 | } 79 | 80 | req, err := http.NewRequest("GET", "/a", nil) 81 | if err != nil { 82 | t.Fatalf("Could not create HTTP request: %v", err) 83 | } 84 | 85 | rec := httptest.NewRecorder() 86 | he.ServeHTTP(rec, req) 87 | 88 | desiredHeaders := []string{"; rel=preload", "; rel=preload"} 89 | actualHeaders := rec.HeaderMap[http.CanonicalHeaderKey("Link")] 90 | sort.Strings(actualHeaders) 91 | 92 | if !reflect.DeepEqual(desiredHeaders, actualHeaders) { 93 | t.Errorf("Expected header to contain: %v but got: %v", desiredHeaders, actualHeaders) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /caddyhttp/httpserver/pathcleaner.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "math/rand" 5 | "path" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // CleanMaskedPath prevents one or more of the path cleanup operations: 11 | // - collapse multiple slashes into one 12 | // - eliminate "/." (current directory) 13 | // - eliminate "/.." 14 | // by masking certain patterns in the path with a temporary random string. 15 | // This could be helpful when certain patterns in the path are desired to be preserved 16 | // that would otherwise be changed by path.Clean(). 17 | // One such use case is the presence of the double slashes as protocol separator 18 | // (e.g., /api/endpoint/http://example.com). 19 | // This is a common pattern in many applications to allow passing URIs as path argument. 20 | func CleanMaskedPath(reqPath string, masks ...string) string { 21 | var replacerVal string 22 | maskMap := make(map[string]string) 23 | 24 | // Iterate over supplied masks and create temporary replacement strings 25 | // only for the masks that are present in the path, then replace all occurrences 26 | for _, mask := range masks { 27 | if strings.Index(reqPath, mask) >= 0 { 28 | replacerVal = "/_caddy" + generateRandomString() + "__" 29 | maskMap[mask] = replacerVal 30 | reqPath = strings.Replace(reqPath, mask, replacerVal, -1) 31 | } 32 | } 33 | 34 | reqPath = path.Clean(reqPath) 35 | 36 | // Revert the replaced masks after path cleanup 37 | for mask, replacerVal := range maskMap { 38 | reqPath = strings.Replace(reqPath, replacerVal, mask, -1) 39 | } 40 | return reqPath 41 | } 42 | 43 | // CleanPath calls CleanMaskedPath() with the default mask of "://" 44 | // to preserve double slashes of protocols 45 | // such as "http://", "https://", and "ftp://" etc. 46 | func CleanPath(reqPath string) string { 47 | return CleanMaskedPath(reqPath, "://") 48 | } 49 | 50 | // An efficient and fast method for random string generation. 51 | // Inspired by http://stackoverflow.com/a/31832326. 52 | const randomStringLength = 4 53 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 54 | const ( 55 | letterIdxBits = 6 56 | letterIdxMask = 1<= 0; { 65 | if remain == 0 { 66 | cache, remain = src.Int63(), letterIdxMax 67 | } 68 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 69 | b[i] = letterBytes[idx] 70 | i-- 71 | } 72 | cache >>= letterIdxBits 73 | remain-- 74 | } 75 | return string(b) 76 | } 77 | -------------------------------------------------------------------------------- /dist/init/freebsd/caddy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # PROVIDE: caddy 4 | # REQUIRE: networking 5 | # KEYWORD: shutdown 6 | 7 | # 8 | # Add the following lines to /etc/rc.conf to enable caddy: 9 | # caddy_enable (bool): Set to "NO" by default. 10 | # Set it to "YES" to enable caddy 11 | # 12 | # caddy_cert_email (str): Set to "" by default. 13 | # Defines the SSL certificate issuer email. By providing an 14 | # email address you automatically agree to letsencrypt.org's 15 | # general terms and conditions 16 | # 17 | # caddy_bin_path (str): Set to "/usr/local/bin/caddy" by default. 18 | # Provides the path to the caddy server executable 19 | # 20 | # caddy_cpu (str): Set to "99%" by default. 21 | # Configures, how much CPU capacity caddy may gain 22 | # 23 | # caddy_config_path (str): Set to "/usr/local/www/Caddyfile" by default. 24 | # Defines the path for the configuration file caddy will load on boot 25 | # 26 | # caddy_run_user (str): Set to "root" by default. 27 | # Defines the user that caddy will run on 28 | # 29 | 30 | . /etc/rc.subr 31 | 32 | name="caddy" 33 | rcvar="${name}_enable" 34 | 35 | load_rc_config $name 36 | : ${caddy_enable:=no} 37 | : ${caddy_cert_email=""} 38 | : ${caddy_bin_path="/usr/local/bin/caddy"} 39 | : ${caddy_cpu="99%"} # was a bug for me that caused a crash within jails 40 | : ${caddy_config_path="/usr/local/www/Caddyfile"} 41 | : ${caddy_run_user="root"} 42 | 43 | if [ "$caddy_cert_email" = "" ] 44 | then 45 | echo "rc variable \$caddy_cert_email is not set. Please provide a valid SSL certificate issuer email." 46 | exit 1 47 | fi 48 | 49 | pidfile="/var/run/caddy.pid" 50 | logfile="/var/log/caddy.log" 51 | 52 | command="${caddy_bin_path} -log ${logfile} -cpu ${caddy_cpu} -conf ${caddy_config_path} -agree -email ${caddy_cert_email}" 53 | 54 | start_cmd="caddy_start" 55 | status_cmd="caddy_status" 56 | stop_cmd="caddy_stop" 57 | 58 | caddy_start() { 59 | echo "Starting ${name}..." 60 | /usr/sbin/daemon -u ${caddy_run_user} -c -p ${pidfile} -f ${command} 61 | } 62 | 63 | caddy_status() { 64 | if [ -f ${pidfile} ]; then 65 | echo "${name} is running as $(cat $pidfile)." 66 | else 67 | echo "${name} is not running." 68 | return 1 69 | fi 70 | } 71 | 72 | caddy_stop() { 73 | if [ ! -f ${pidfile} ]; then 74 | echo "${name} is not running." 75 | return 1 76 | fi 77 | 78 | echo -n "Stopping ${name}..." 79 | kill -KILL $(cat $pidfile) 2> /dev/null && echo "stopped" 80 | rm -f ${pidfile} 81 | } 82 | 83 | run_rc_command "$1" 84 | -------------------------------------------------------------------------------- /caddyhttp/push/handler.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func (h Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 11 | pusher, hasPusher := w.(http.Pusher) 12 | 13 | // no push possible, carry on 14 | if !hasPusher { 15 | return h.Next.ServeHTTP(w, r) 16 | } 17 | 18 | // check if this is a request for the pushed resource (avoid recursion) 19 | if _, exists := r.Header[pushHeader]; exists { 20 | return h.Next.ServeHTTP(w, r) 21 | } 22 | 23 | headers := h.filterProxiedHeaders(r.Header) 24 | 25 | // push first 26 | outer: 27 | for _, rule := range h.Rules { 28 | if httpserver.Path(r.URL.Path).Matches(rule.Path) { 29 | for _, resource := range rule.Resources { 30 | pushErr := pusher.Push(resource.Path, &http.PushOptions{ 31 | Method: resource.Method, 32 | Header: h.mergeHeaders(headers, resource.Header), 33 | }) 34 | if pushErr != nil { 35 | // if we cannot push (either not supported or concurrent streams are full - break) 36 | break outer 37 | } 38 | } 39 | } 40 | } 41 | 42 | // serve later 43 | code, err := h.Next.ServeHTTP(w, r) 44 | 45 | // push resources returned in Link headers from upstream middlewares or proxied apps 46 | if links, exists := w.Header()["Link"]; exists { 47 | h.servePreloadLinks(pusher, headers, links) 48 | } 49 | 50 | return code, err 51 | } 52 | 53 | func (h Middleware) servePreloadLinks(pusher http.Pusher, headers http.Header, links []string) { 54 | for _, link := range links { 55 | parts := strings.Split(link, ";") 56 | 57 | if link == "" || strings.HasSuffix(link, "nopush") { 58 | continue 59 | } 60 | 61 | target := strings.TrimSuffix(strings.TrimPrefix(parts[0], "<"), ">") 62 | 63 | err := pusher.Push(target, &http.PushOptions{ 64 | Method: http.MethodGet, 65 | Header: headers, 66 | }) 67 | 68 | if err != nil { 69 | break 70 | } 71 | } 72 | } 73 | 74 | func (h Middleware) mergeHeaders(l, r http.Header) http.Header { 75 | out := http.Header{} 76 | 77 | for k, v := range l { 78 | out[k] = v 79 | } 80 | 81 | for k, vv := range r { 82 | for _, v := range vv { 83 | out.Add(k, v) 84 | } 85 | } 86 | 87 | return out 88 | } 89 | 90 | func (h Middleware) filterProxiedHeaders(headers http.Header) http.Header { 91 | filter := http.Header{} 92 | 93 | for _, header := range proxiedHeaders { 94 | if val, ok := headers[header]; ok { 95 | filter[header] = val 96 | } 97 | } 98 | 99 | return filter 100 | } 101 | 102 | var proxiedHeaders = []string{ 103 | "Accept-Encoding", 104 | "Accept-Language", 105 | "Cache-Control", 106 | "Host", 107 | "User-Agent", 108 | } 109 | -------------------------------------------------------------------------------- /caddyhttp/browse/setup_test.go: -------------------------------------------------------------------------------- 1 | package browse 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/mholt/caddy" 12 | "github.com/mholt/caddy/caddyhttp/httpserver" 13 | ) 14 | 15 | func TestSetup(t *testing.T) { 16 | tempDirPath := os.TempDir() 17 | _, err := os.Stat(tempDirPath) 18 | if err != nil { 19 | t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) 20 | } 21 | nonExistentDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano()))) 22 | 23 | tempTemplate, err := ioutil.TempFile(".", "tempTemplate") 24 | if err != nil { 25 | t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err) 26 | } 27 | defer os.Remove(tempTemplate.Name()) 28 | 29 | tempTemplatePath := filepath.Join(".", tempTemplate.Name()) 30 | 31 | for i, test := range []struct { 32 | input string 33 | expectedPathScope []string 34 | shouldErr bool 35 | }{ 36 | // test case #0 tests handling of multiple pathscopes 37 | {"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false}, 38 | 39 | // test case #1 tests instantiation of Config with default values 40 | {"browse /", []string{"/"}, false}, 41 | 42 | // test case #2 tests detectaction of custom template 43 | {"browse . " + tempTemplatePath, []string{"."}, false}, 44 | 45 | // test case #3 tests detection of non-existent template 46 | {"browse . " + nonExistentDirPath, nil, true}, 47 | 48 | // test case #4 tests detection of duplicate pathscopes 49 | {"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true}, 50 | } { 51 | 52 | c := caddy.NewTestController("http", test.input) 53 | err := setup(c) 54 | if err != nil && !test.shouldErr { 55 | t.Errorf("Test case #%d received an error of %v", i, err) 56 | } 57 | if test.expectedPathScope == nil { 58 | continue 59 | } 60 | mids := httpserver.GetConfig(c).Middleware() 61 | mid := mids[len(mids)-1] 62 | receivedConfigs := mid(nil).(Browse).Configs 63 | for j, config := range receivedConfigs { 64 | if config.PathScope != test.expectedPathScope[j] { 65 | t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope) 66 | } 67 | } 68 | } 69 | 70 | // test case #6 tests startup with missing root directory in combination with default browse settings 71 | controller := caddy.NewTestController("http", "browse") 72 | cfg := httpserver.GetConfig(controller) 73 | 74 | // Make sure non-existent root path doesn't return error 75 | cfg.Root = nonExistentDirPath 76 | err = setup(controller) 77 | 78 | if err != nil { 79 | t.Errorf("Test for non-existent browse path received an error, but shouldn't have: %v", err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /caddyhttp/log/log.go: -------------------------------------------------------------------------------- 1 | // Package log implements request (access) logging middleware. 2 | package log 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/mholt/caddy" 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | func init() { 13 | caddy.RegisterPlugin("log", caddy.Plugin{ 14 | ServerType: "http", 15 | Action: setup, 16 | }) 17 | } 18 | 19 | // Logger is a basic request logging middleware. 20 | type Logger struct { 21 | Next httpserver.Handler 22 | Rules []*Rule 23 | ErrorFunc func(http.ResponseWriter, *http.Request, int) // failover error handler 24 | } 25 | 26 | func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 27 | for _, rule := range l.Rules { 28 | if httpserver.Path(r.URL.Path).Matches(rule.PathScope) { 29 | // Record the response 30 | responseRecorder := httpserver.NewResponseRecorder(w) 31 | 32 | // Attach the Replacer we'll use so that other middlewares can 33 | // set their own placeholders if they want to. 34 | rep := httpserver.NewReplacer(r, responseRecorder, CommonLogEmptyValue) 35 | responseRecorder.Replacer = rep 36 | 37 | // Bon voyage, request! 38 | status, err := l.Next.ServeHTTP(responseRecorder, r) 39 | 40 | if status >= 400 { 41 | // There was an error up the chain, but no response has been written yet. 42 | // The error must be handled here so the log entry will record the response size. 43 | if l.ErrorFunc != nil { 44 | l.ErrorFunc(responseRecorder, r, status) 45 | } else { 46 | // Default failover error handler 47 | responseRecorder.WriteHeader(status) 48 | fmt.Fprintf(responseRecorder, "%d %s", status, http.StatusText(status)) 49 | } 50 | status = 0 51 | } 52 | 53 | // Write log entries 54 | for _, e := range rule.Entries { 55 | e.Log.Println(rep.Replace(e.Format)) 56 | } 57 | 58 | return status, err 59 | } 60 | } 61 | return l.Next.ServeHTTP(w, r) 62 | } 63 | 64 | // Entry represents a log entry under a path scope 65 | type Entry struct { 66 | Format string 67 | Log *httpserver.Logger 68 | } 69 | 70 | // Rule configures the logging middleware. 71 | type Rule struct { 72 | PathScope string 73 | Entries []*Entry 74 | } 75 | 76 | const ( 77 | // DefaultLogFilename is the default log filename. 78 | DefaultLogFilename = "access.log" 79 | // CommonLogFormat is the common log format. 80 | CommonLogFormat = `{remote} ` + CommonLogEmptyValue + " " + CommonLogEmptyValue + ` [{when}] "{method} {uri} {proto}" {status} {size}` 81 | // CommonLogEmptyValue is the common empty log value. 82 | CommonLogEmptyValue = "-" 83 | // CombinedLogFormat is the combined log format. 84 | CombinedLogFormat = CommonLogFormat + ` "{>Referer}" "{>User-Agent}"` 85 | // DefaultLogFormat is the default log format. 86 | DefaultLogFormat = CommonLogFormat 87 | ) 88 | -------------------------------------------------------------------------------- /caddyhttp/gzip/requestfilter.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | // RequestFilter determines if a request should be gzipped. 11 | type RequestFilter interface { 12 | // ShouldCompress tells if gzip compression 13 | // should be done on the request. 14 | ShouldCompress(*http.Request) bool 15 | } 16 | 17 | // defaultExtensions is the list of default extensions for which to enable gzipping. 18 | var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json", 19 | ".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp"} 20 | 21 | // DefaultExtFilter creates an ExtFilter with default extensions. 22 | func DefaultExtFilter() ExtFilter { 23 | m := ExtFilter{Exts: make(Set)} 24 | for _, extension := range defaultExtensions { 25 | m.Exts.Add(extension) 26 | } 27 | return m 28 | } 29 | 30 | // ExtFilter is RequestFilter for file name extensions. 31 | type ExtFilter struct { 32 | // Exts is the file name extensions to accept 33 | Exts Set 34 | } 35 | 36 | // ExtWildCard is the wildcard for extensions. 37 | const ExtWildCard = "*" 38 | 39 | // ShouldCompress checks if the request file extension matches any 40 | // of the registered extensions. It returns true if the extension is 41 | // found and false otherwise. 42 | func (e ExtFilter) ShouldCompress(r *http.Request) bool { 43 | ext := path.Ext(r.URL.Path) 44 | return e.Exts.Contains(ExtWildCard) || e.Exts.Contains(ext) 45 | } 46 | 47 | // PathFilter is RequestFilter for request path. 48 | type PathFilter struct { 49 | // IgnoredPaths is the paths to ignore 50 | IgnoredPaths Set 51 | } 52 | 53 | // ShouldCompress checks if the request path matches any of the 54 | // registered paths to ignore. It returns false if an ignored path 55 | // is found and true otherwise. 56 | func (p PathFilter) ShouldCompress(r *http.Request) bool { 57 | return !p.IgnoredPaths.ContainsFunc(func(value string) bool { 58 | return httpserver.Path(r.URL.Path).Matches(value) 59 | }) 60 | } 61 | 62 | // Set stores distinct strings. 63 | type Set map[string]struct{} 64 | 65 | // Add adds an element to the set. 66 | func (s Set) Add(value string) { 67 | s[value] = struct{}{} 68 | } 69 | 70 | // Remove removes an element from the set. 71 | func (s Set) Remove(value string) { 72 | delete(s, value) 73 | } 74 | 75 | // Contains check if the set contains value. 76 | func (s Set) Contains(value string) bool { 77 | _, ok := s[value] 78 | return ok 79 | } 80 | 81 | // ContainsFunc is similar to Contains. It iterates all the 82 | // elements in the set and passes each to f. It returns true 83 | // on the first call to f that returns true and false otherwise. 84 | func (s Set) ContainsFunc(f func(string) bool) bool { 85 | for k := range s { 86 | if f(k) { 87 | return true 88 | } 89 | } 90 | return false 91 | } 92 | -------------------------------------------------------------------------------- /caddyhttp/header/setup_test.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/mholt/caddy" 10 | "github.com/mholt/caddy/caddyhttp/httpserver" 11 | ) 12 | 13 | func TestSetup(t *testing.T) { 14 | c := caddy.NewTestController("http", `header / Foo Bar`) 15 | err := setup(c) 16 | if err != nil { 17 | t.Errorf("Expected no errors, but got: %v", err) 18 | } 19 | 20 | mids := httpserver.GetConfig(c).Middleware() 21 | if len(mids) == 0 { 22 | t.Fatal("Expected middleware, had 0 instead") 23 | } 24 | 25 | handler := mids[0](httpserver.EmptyNext) 26 | myHandler, ok := handler.(Headers) 27 | if !ok { 28 | t.Fatalf("Expected handler to be type Headers, got: %#v", handler) 29 | } 30 | 31 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 32 | t.Error("'Next' field of handler was not set properly") 33 | } 34 | } 35 | 36 | func TestHeadersParse(t *testing.T) { 37 | tests := []struct { 38 | input string 39 | shouldErr bool 40 | expected []Rule 41 | }{ 42 | {`header /foo Foo "Bar Baz"`, 43 | false, []Rule{ 44 | {Path: "/foo", Headers: http.Header{ 45 | "Foo": []string{"Bar Baz"}, 46 | }}, 47 | }}, 48 | {`header /bar { 49 | Foo "Bar Baz" 50 | Baz Qux 51 | Foobar 52 | }`, 53 | false, []Rule{ 54 | {Path: "/bar", Headers: http.Header{ 55 | "Foo": []string{"Bar Baz"}, 56 | "Baz": []string{"Qux"}, 57 | "Foobar": []string{""}, 58 | }}, 59 | }}, 60 | {`header /foo { 61 | Foo Bar Baz 62 | }`, true, 63 | []Rule{}}, 64 | {`header /foo { 65 | Test "max-age=1814400"; 66 | }`, true, []Rule{}}, 67 | } 68 | 69 | for i, test := range tests { 70 | actual, err := headersParse(caddy.NewTestController("http", test.input)) 71 | 72 | if err == nil && test.shouldErr { 73 | t.Errorf("Test %d didn't error, but it should have", i) 74 | } else if err != nil && !test.shouldErr { 75 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 76 | } 77 | 78 | if len(actual) != len(test.expected) { 79 | t.Fatalf("Test %d expected %d rules, but got %d", 80 | i, len(test.expected), len(actual)) 81 | } 82 | 83 | for j, expectedRule := range test.expected { 84 | actualRule := actual[j] 85 | 86 | if actualRule.Path != expectedRule.Path { 87 | t.Errorf("Test %d, rule %d: Expected path %s, but got %s", 88 | i, j, expectedRule.Path, actualRule.Path) 89 | } 90 | 91 | expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers) 92 | actualHeaders := fmt.Sprintf("%v", actualRule.Headers) 93 | 94 | if !reflect.DeepEqual(actualRule.Headers, expectedRule.Headers) { 95 | t.Errorf("Test %d, rule %d: Expected headers %s, but got %s", 96 | i, j, expectedHeaders, actualHeaders) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /caddyhttp/log/setup.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/mholt/caddy" 5 | "github.com/mholt/caddy/caddyhttp/httpserver" 6 | ) 7 | 8 | // setup sets up the logging middleware. 9 | func setup(c *caddy.Controller) error { 10 | rules, err := logParse(c) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | for _, rule := range rules { 16 | for _, entry := range rule.Entries { 17 | entry.Log.Attach(c) 18 | } 19 | } 20 | 21 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 22 | return Logger{Next: next, Rules: rules, ErrorFunc: httpserver.DefaultErrorFunc} 23 | }) 24 | 25 | return nil 26 | } 27 | 28 | func logParse(c *caddy.Controller) ([]*Rule, error) { 29 | var rules []*Rule 30 | 31 | for c.Next() { 32 | args := c.RemainingArgs() 33 | 34 | var logRoller *httpserver.LogRoller 35 | logRoller = httpserver.DefaultLogRoller() 36 | 37 | for c.NextBlock() { 38 | what := c.Val() 39 | if !c.NextArg() { 40 | return nil, c.ArgErr() 41 | } 42 | where := c.Val() 43 | 44 | // only support roller related options inside a block 45 | if !httpserver.IsLogRollerSubdirective(what) { 46 | return nil, c.ArgErr() 47 | } 48 | 49 | if err := httpserver.ParseRoller(logRoller, what, where); err != nil { 50 | return nil, err 51 | } 52 | } 53 | 54 | if len(args) == 0 { 55 | // Nothing specified; use defaults 56 | rules = appendEntry(rules, "/", &Entry{ 57 | Log: &httpserver.Logger{ 58 | Output: DefaultLogFilename, 59 | Roller: logRoller, 60 | }, 61 | Format: DefaultLogFormat, 62 | }) 63 | } else if len(args) == 1 { 64 | // Only an output file specified 65 | rules = appendEntry(rules, "/", &Entry{ 66 | Log: &httpserver.Logger{ 67 | Output: args[0], 68 | Roller: logRoller, 69 | }, 70 | Format: DefaultLogFormat, 71 | }) 72 | } else { 73 | // Path scope, output file, and maybe a format specified 74 | 75 | format := DefaultLogFormat 76 | 77 | if len(args) > 2 { 78 | switch args[2] { 79 | case "{common}": 80 | format = CommonLogFormat 81 | case "{combined}": 82 | format = CombinedLogFormat 83 | default: 84 | format = args[2] 85 | } 86 | } 87 | 88 | rules = appendEntry(rules, args[0], &Entry{ 89 | Log: &httpserver.Logger{ 90 | Output: args[1], 91 | Roller: logRoller, 92 | }, 93 | Format: format, 94 | }) 95 | } 96 | } 97 | 98 | return rules, nil 99 | } 100 | 101 | func appendEntry(rules []*Rule, pathScope string, entry *Entry) []*Rule { 102 | for _, rule := range rules { 103 | if rule.PathScope == pathScope { 104 | rule.Entries = append(rule.Entries, entry) 105 | return rules 106 | } 107 | } 108 | 109 | rules = append(rules, &Rule{ 110 | PathScope: pathScope, 111 | Entries: []*Entry{entry}, 112 | }) 113 | 114 | return rules 115 | } 116 | -------------------------------------------------------------------------------- /caddytls/certificates_test.go: -------------------------------------------------------------------------------- 1 | package caddytls 2 | 3 | import "testing" 4 | 5 | func TestUnexportedGetCertificate(t *testing.T) { 6 | defer func() { certCache = make(map[string]Certificate) }() 7 | 8 | // When cache is empty 9 | if _, matched, defaulted := getCertificate("example.com"); matched || defaulted { 10 | t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted) 11 | } 12 | 13 | // When cache has one certificate in it (also is default) 14 | defaultCert := Certificate{Names: []string{"example.com", ""}} 15 | certCache[""] = defaultCert 16 | certCache["example.com"] = defaultCert 17 | if cert, matched, defaulted := getCertificate("Example.com"); !matched || defaulted || cert.Names[0] != "example.com" { 18 | t.Errorf("Didn't get a cert for 'Example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) 19 | } 20 | if cert, matched, defaulted := getCertificate(""); !matched || defaulted || cert.Names[0] != "example.com" { 21 | t.Errorf("Didn't get a cert for '' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) 22 | } 23 | 24 | // When retrieving wildcard certificate 25 | certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}} 26 | if cert, matched, defaulted := getCertificate("sub.example.com"); !matched || defaulted || cert.Names[0] != "*.example.com" { 27 | t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted) 28 | } 29 | 30 | // When no certificate matches, the default is returned 31 | if cert, matched, defaulted := getCertificate("nomatch"); matched || !defaulted { 32 | t.Errorf("Expected matched=false, defaulted=true; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert) 33 | } else if cert.Names[0] != "example.com" { 34 | t.Errorf("Expected default cert, got: %v", cert) 35 | } 36 | } 37 | 38 | func TestCacheCertificate(t *testing.T) { 39 | defer func() { certCache = make(map[string]Certificate) }() 40 | 41 | cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}}) 42 | if _, ok := certCache["example.com"]; !ok { 43 | t.Error("Expected first cert to be cached by key 'example.com', but it wasn't") 44 | } 45 | if _, ok := certCache["sub.example.com"]; !ok { 46 | t.Error("Expected first cert to be cached by key 'sub.example.com', but it wasn't") 47 | } 48 | if cert, ok := certCache[""]; !ok || cert.Names[2] != "" { 49 | t.Error("Expected first cert to be cached additionally as the default certificate with empty name added, but it wasn't") 50 | } 51 | 52 | cacheCertificate(Certificate{Names: []string{"example2.com"}}) 53 | if _, ok := certCache["example2.com"]; !ok { 54 | t.Error("Expected second cert to be cached by key 'exmaple2.com', but it wasn't") 55 | } 56 | if cert, ok := certCache[""]; ok && cert.Names[0] == "example2.com" { 57 | t.Error("Expected second cert to NOT be cached as default, but it was") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /caddyhttp/gzip/requestfilter_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestSet(t *testing.T) { 9 | set := make(Set) 10 | set.Add("a") 11 | if len(set) != 1 { 12 | t.Errorf("Expected 1 found %v", len(set)) 13 | } 14 | set.Add("a") 15 | if len(set) != 1 { 16 | t.Errorf("Expected 1 found %v", len(set)) 17 | } 18 | set.Add("b") 19 | if len(set) != 2 { 20 | t.Errorf("Expected 2 found %v", len(set)) 21 | } 22 | if !set.Contains("a") { 23 | t.Errorf("Set should contain a") 24 | } 25 | if !set.Contains("b") { 26 | t.Errorf("Set should contain a") 27 | } 28 | set.Add("c") 29 | if len(set) != 3 { 30 | t.Errorf("Expected 3 found %v", len(set)) 31 | } 32 | if !set.Contains("c") { 33 | t.Errorf("Set should contain c") 34 | } 35 | set.Remove("a") 36 | if len(set) != 2 { 37 | t.Errorf("Expected 2 found %v", len(set)) 38 | } 39 | if set.Contains("a") { 40 | t.Errorf("Set should not contain a") 41 | } 42 | if !set.ContainsFunc(func(v string) bool { 43 | return v == "c" 44 | }) { 45 | t.Errorf("ContainsFunc should return true") 46 | } 47 | } 48 | 49 | func TestExtFilter(t *testing.T) { 50 | var filter RequestFilter = ExtFilter{make(Set)} 51 | for _, e := range []string{".txt", ".html", ".css", ".md"} { 52 | filter.(ExtFilter).Exts.Add(e) 53 | } 54 | r := urlRequest("file.txt") 55 | if !filter.ShouldCompress(r) { 56 | t.Errorf("Should be valid filter") 57 | } 58 | var exts = []string{ 59 | ".html", ".css", ".md", 60 | } 61 | for i, e := range exts { 62 | r := urlRequest("file" + e) 63 | if !filter.ShouldCompress(r) { 64 | t.Errorf("Test %v: Should be valid filter", i) 65 | } 66 | } 67 | exts = []string{ 68 | ".htm1", ".abc", ".mdx", 69 | } 70 | for i, e := range exts { 71 | r := urlRequest("file" + e) 72 | if filter.ShouldCompress(r) { 73 | t.Errorf("Test %v: Should not be valid filter", i) 74 | } 75 | } 76 | filter.(ExtFilter).Exts.Add(ExtWildCard) 77 | for i, e := range exts { 78 | r := urlRequest("file" + e) 79 | if !filter.ShouldCompress(r) { 80 | t.Errorf("Test %v: Should be valid filter. Wildcard used.", i) 81 | } 82 | } 83 | } 84 | 85 | func TestPathFilter(t *testing.T) { 86 | paths := []string{ 87 | "/a", "/b", "/c", "/de", 88 | } 89 | var filter RequestFilter = PathFilter{make(Set)} 90 | for _, p := range paths { 91 | filter.(PathFilter).IgnoredPaths.Add(p) 92 | } 93 | for i, p := range paths { 94 | r := urlRequest(p) 95 | if filter.ShouldCompress(r) { 96 | t.Errorf("Test %v: Should not be valid filter", i) 97 | } 98 | } 99 | paths = []string{ 100 | "/f", "/g", "/h", "/ed", 101 | } 102 | for i, p := range paths { 103 | r := urlRequest(p) 104 | if !filter.ShouldCompress(r) { 105 | t.Errorf("Test %v: Should be valid filter", i) 106 | } 107 | } 108 | } 109 | 110 | func urlRequest(url string) *http.Request { 111 | r, _ := http.NewRequest("GET", url, nil) 112 | return r 113 | } 114 | -------------------------------------------------------------------------------- /caddyhttp/timeouts/timeouts.go: -------------------------------------------------------------------------------- 1 | package timeouts 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func init() { 11 | caddy.RegisterPlugin("timeouts", caddy.Plugin{ 12 | ServerType: "http", 13 | Action: setupTimeouts, 14 | }) 15 | } 16 | 17 | func setupTimeouts(c *caddy.Controller) error { 18 | config := httpserver.GetConfig(c) 19 | 20 | for c.Next() { 21 | var hasOptionalBlock bool 22 | for c.NextBlock() { 23 | hasOptionalBlock = true 24 | 25 | // ensure the kind of timeout is recognized 26 | kind := c.Val() 27 | if kind != "read" && kind != "header" && kind != "write" && kind != "idle" { 28 | return c.Errf("unknown timeout '%s': must be read, header, write, or idle", kind) 29 | } 30 | 31 | // parse the timeout duration 32 | if !c.NextArg() { 33 | return c.ArgErr() 34 | } 35 | if c.NextArg() { 36 | // only one value permitted 37 | return c.ArgErr() 38 | } 39 | var dur time.Duration 40 | if c.Val() != "none" { 41 | var err error 42 | dur, err = time.ParseDuration(c.Val()) 43 | if err != nil { 44 | return c.Errf("%v", err) 45 | } 46 | if dur < 0 { 47 | return c.Err("non-negative duration required for timeout value") 48 | } 49 | } 50 | 51 | // set this timeout's duration 52 | switch kind { 53 | case "read": 54 | config.Timeouts.ReadTimeout = dur 55 | config.Timeouts.ReadTimeoutSet = true 56 | case "header": 57 | config.Timeouts.ReadHeaderTimeout = dur 58 | config.Timeouts.ReadHeaderTimeoutSet = true 59 | case "write": 60 | config.Timeouts.WriteTimeout = dur 61 | config.Timeouts.WriteTimeoutSet = true 62 | case "idle": 63 | config.Timeouts.IdleTimeout = dur 64 | config.Timeouts.IdleTimeoutSet = true 65 | } 66 | } 67 | if !hasOptionalBlock { 68 | // set all timeouts to the same value 69 | 70 | if !c.NextArg() { 71 | return c.ArgErr() 72 | } 73 | if c.NextArg() { 74 | // only one value permitted 75 | return c.ArgErr() 76 | } 77 | val := c.Val() 78 | 79 | config.Timeouts.ReadTimeoutSet = true 80 | config.Timeouts.ReadHeaderTimeoutSet = true 81 | config.Timeouts.WriteTimeoutSet = true 82 | config.Timeouts.IdleTimeoutSet = true 83 | 84 | if val == "none" { 85 | config.Timeouts.ReadTimeout = 0 86 | config.Timeouts.ReadHeaderTimeout = 0 87 | config.Timeouts.WriteTimeout = 0 88 | config.Timeouts.IdleTimeout = 0 89 | } else { 90 | dur, err := time.ParseDuration(val) 91 | if err != nil { 92 | return c.Errf("unknown timeout duration: %v", err) 93 | } 94 | if dur < 0 { 95 | return c.Err("non-negative duration required for timeout value") 96 | } 97 | config.Timeouts.ReadTimeout = dur 98 | config.Timeouts.ReadHeaderTimeout = dur 99 | config.Timeouts.WriteTimeout = dur 100 | config.Timeouts.IdleTimeout = dur 101 | } 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /caddyhttp/errors/setup.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | 9 | "github.com/mholt/caddy" 10 | "github.com/mholt/caddy/caddyhttp/httpserver" 11 | ) 12 | 13 | // setup configures a new errors middleware instance. 14 | func setup(c *caddy.Controller) error { 15 | handler, err := errorsParse(c) 16 | 17 | if err != nil { 18 | return err 19 | } 20 | 21 | handler.Log.Attach(c) 22 | 23 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 24 | handler.Next = next 25 | return handler 26 | }) 27 | 28 | return nil 29 | } 30 | 31 | func errorsParse(c *caddy.Controller) (*ErrorHandler, error) { 32 | 33 | // Very important that we make a pointer because the startup 34 | // function that opens the log file must have access to the 35 | // same instance of the handler, not a copy. 36 | handler := &ErrorHandler{ 37 | ErrorPages: make(map[int]string), 38 | Log: &httpserver.Logger{}, 39 | } 40 | 41 | cfg := httpserver.GetConfig(c) 42 | 43 | optionalBlock := func() error { 44 | for c.NextBlock() { 45 | 46 | what := c.Val() 47 | if !c.NextArg() { 48 | return c.ArgErr() 49 | } 50 | where := c.Val() 51 | 52 | if httpserver.IsLogRollerSubdirective(what) { 53 | var err error 54 | err = httpserver.ParseRoller(handler.Log.Roller, what, where) 55 | if err != nil { 56 | return err 57 | } 58 | } else { 59 | // Error page; ensure it exists 60 | if !filepath.IsAbs(where) { 61 | where = filepath.Join(cfg.Root, where) 62 | } 63 | 64 | f, err := os.Open(where) 65 | if err != nil { 66 | log.Printf("[WARNING] Unable to open error page '%s': %v", where, err) 67 | } 68 | f.Close() 69 | 70 | if what == "*" { 71 | if handler.GenericErrorPage != "" { 72 | return c.Errf("Duplicate status code entry: %s", what) 73 | } 74 | handler.GenericErrorPage = where 75 | } else { 76 | whatInt, err := strconv.Atoi(what) 77 | if err != nil { 78 | return c.Err("Expecting a numeric status code or '*', got '" + what + "'") 79 | } 80 | 81 | if _, exists := handler.ErrorPages[whatInt]; exists { 82 | return c.Errf("Duplicate status code entry: %s", what) 83 | } 84 | 85 | handler.ErrorPages[whatInt] = where 86 | } 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | for c.Next() { 93 | // weird hack to avoid having the handler values overwritten. 94 | if c.Val() == "}" { 95 | continue 96 | } 97 | 98 | args := c.RemainingArgs() 99 | 100 | if len(args) == 1 { 101 | switch args[0] { 102 | case "visible": 103 | handler.Debug = true 104 | default: 105 | handler.Log.Output = args[0] 106 | handler.Log.Roller = httpserver.DefaultLogRoller() 107 | } 108 | } 109 | 110 | // Configuration may be in a block 111 | err := optionalBlock() 112 | if err != nil { 113 | return handler, err 114 | } 115 | } 116 | 117 | return handler, nil 118 | } 119 | -------------------------------------------------------------------------------- /caddyhttp/gzip/responsefilter.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "compress/gzip" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | // ResponseFilter determines if the response should be gzipped. 10 | type ResponseFilter interface { 11 | ShouldCompress(http.ResponseWriter) bool 12 | } 13 | 14 | // LengthFilter is ResponseFilter for minimum content length. 15 | type LengthFilter int64 16 | 17 | // ShouldCompress returns if content length is greater than or 18 | // equals to minimum length. 19 | func (l LengthFilter) ShouldCompress(w http.ResponseWriter) bool { 20 | contentLength := w.Header().Get("Content-Length") 21 | length, err := strconv.ParseInt(contentLength, 10, 64) 22 | if err != nil || length == 0 { 23 | return false 24 | } 25 | return l != 0 && int64(l) <= length 26 | } 27 | 28 | // SkipCompressedFilter is ResponseFilter that will discard already compressed responses 29 | type SkipCompressedFilter struct{} 30 | 31 | // ShouldCompress returns true if served file is not already compressed 32 | // encodings via https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding 33 | func (n SkipCompressedFilter) ShouldCompress(w http.ResponseWriter) bool { 34 | switch w.Header().Get("Content-Encoding") { 35 | case "gzip", "compress", "deflate", "br": 36 | return false 37 | default: 38 | return true 39 | } 40 | } 41 | 42 | // ResponseFilterWriter validates ResponseFilters. It writes 43 | // gzip compressed data if ResponseFilters are satisfied or 44 | // uncompressed data otherwise. 45 | type ResponseFilterWriter struct { 46 | filters []ResponseFilter 47 | shouldCompress bool 48 | statusCodeWritten bool 49 | *gzipResponseWriter 50 | } 51 | 52 | // NewResponseFilterWriter creates and initializes a new ResponseFilterWriter. 53 | func NewResponseFilterWriter(filters []ResponseFilter, gz *gzipResponseWriter) *ResponseFilterWriter { 54 | return &ResponseFilterWriter{filters: filters, gzipResponseWriter: gz} 55 | } 56 | 57 | // WriteHeader wraps underlying WriteHeader method and 58 | // compresses if filters are satisfied. 59 | func (r *ResponseFilterWriter) WriteHeader(code int) { 60 | // Determine if compression should be used or not. 61 | r.shouldCompress = true 62 | for _, filter := range r.filters { 63 | if !filter.ShouldCompress(r) { 64 | r.shouldCompress = false 65 | break 66 | } 67 | } 68 | 69 | if r.shouldCompress { 70 | // replace discard writer with ResponseWriter 71 | if gzWriter, ok := r.gzipResponseWriter.Writer.(*gzip.Writer); ok { 72 | gzWriter.Reset(r.ResponseWriter) 73 | } 74 | // use gzip WriteHeader to include and delete 75 | // necessary headers 76 | r.gzipResponseWriter.WriteHeader(code) 77 | } else { 78 | r.ResponseWriter.WriteHeader(code) 79 | } 80 | r.statusCodeWritten = true 81 | } 82 | 83 | // Write wraps underlying Write method and compresses if filters 84 | // are satisfied 85 | func (r *ResponseFilterWriter) Write(b []byte) (int, error) { 86 | if !r.statusCodeWritten { 87 | r.WriteHeader(http.StatusOK) 88 | } 89 | if r.shouldCompress { 90 | return r.gzipResponseWriter.Write(b) 91 | } 92 | return r.ResponseWriter.Write(b) 93 | } 94 | -------------------------------------------------------------------------------- /caddyhttp/httpserver/roller.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "io" 5 | "path/filepath" 6 | "strconv" 7 | 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | ) 10 | 11 | // LogRoller implements a type that provides a rolling logger. 12 | type LogRoller struct { 13 | Filename string 14 | MaxSize int 15 | MaxAge int 16 | MaxBackups int 17 | LocalTime bool 18 | } 19 | 20 | // GetLogWriter returns an io.Writer that writes to a rolling logger. 21 | // This should be called only from the main goroutine (like during 22 | // server setup) because this method is not thread-safe; it is careful 23 | // to create only one log writer per log file, even if the log file 24 | // is shared by different sites or middlewares. This ensures that 25 | // rolling is synchronized, since a process (or multiple processes) 26 | // should not create more than one roller on the same file at the 27 | // same time. See issue #1363. 28 | func (l LogRoller) GetLogWriter() io.Writer { 29 | absPath, err := filepath.Abs(l.Filename) 30 | if err != nil { 31 | absPath = l.Filename // oh well, hopefully they're consistent in how they specify the filename 32 | } 33 | lj, has := lumberjacks[absPath] 34 | if !has { 35 | lj = &lumberjack.Logger{ 36 | Filename: l.Filename, 37 | MaxSize: l.MaxSize, 38 | MaxAge: l.MaxAge, 39 | MaxBackups: l.MaxBackups, 40 | LocalTime: l.LocalTime, 41 | } 42 | lumberjacks[absPath] = lj 43 | } 44 | return lj 45 | } 46 | 47 | // IsLogRollerSubdirective is true if the subdirective is for the log roller. 48 | func IsLogRollerSubdirective(subdir string) bool { 49 | return subdir == directiveRotateSize || 50 | subdir == directiveRotateAge || 51 | subdir == directiveRotateKeep 52 | } 53 | 54 | // ParseRoller parses roller contents out of c. 55 | func ParseRoller(l *LogRoller, what string, where string) error { 56 | if l == nil { 57 | l = DefaultLogRoller() 58 | } 59 | var value int 60 | var err error 61 | value, err = strconv.Atoi(where) 62 | if err != nil { 63 | return err 64 | } 65 | switch what { 66 | case directiveRotateSize: 67 | l.MaxSize = value 68 | case directiveRotateAge: 69 | l.MaxAge = value 70 | case directiveRotateKeep: 71 | l.MaxBackups = value 72 | } 73 | return nil 74 | } 75 | 76 | // DefaultLogRoller will roll logs by default. 77 | func DefaultLogRoller() *LogRoller { 78 | return &LogRoller{ 79 | MaxSize: defaultRotateSize, 80 | MaxAge: defaultRotateAge, 81 | MaxBackups: defaultRotateKeep, 82 | LocalTime: true, 83 | } 84 | } 85 | 86 | const ( 87 | // defaultRotateSize is 100 MB. 88 | defaultRotateSize = 100 89 | // defaultRotateAge is 14 days. 90 | defaultRotateAge = 14 91 | // defaultRotateKeep is 10 files. 92 | defaultRotateKeep = 10 93 | directiveRotateSize = "rotate_size" 94 | directiveRotateAge = "rotate_age" 95 | directiveRotateKeep = "rotate_keep" 96 | ) 97 | 98 | // lumberjacks maps log filenames to the logger 99 | // that is being used to keep them rolled/maintained. 100 | var lumberjacks = make(map[string]*lumberjack.Logger) 101 | -------------------------------------------------------------------------------- /caddyfile/lexer.go: -------------------------------------------------------------------------------- 1 | package caddyfile 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "unicode" 7 | ) 8 | 9 | type ( 10 | // lexer is a utility which can get values, token by 11 | // token, from a Reader. A token is a word, and tokens 12 | // are separated by whitespace. A word can be enclosed 13 | // in quotes if it contains whitespace. 14 | lexer struct { 15 | reader *bufio.Reader 16 | token Token 17 | line int 18 | } 19 | 20 | // Token represents a single parsable unit. 21 | Token struct { 22 | File string 23 | Line int 24 | Text string 25 | } 26 | ) 27 | 28 | // load prepares the lexer to scan an input for tokens. 29 | // It discards any leading byte order mark. 30 | func (l *lexer) load(input io.Reader) error { 31 | l.reader = bufio.NewReader(input) 32 | l.line = 1 33 | 34 | // discard byte order mark, if present 35 | firstCh, _, err := l.reader.ReadRune() 36 | if err != nil { 37 | return err 38 | } 39 | if firstCh != 0xFEFF { 40 | err := l.reader.UnreadRune() 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | 49 | // next loads the next token into the lexer. 50 | // A token is delimited by whitespace, unless 51 | // the token starts with a quotes character (") 52 | // in which case the token goes until the closing 53 | // quotes (the enclosing quotes are not included). 54 | // Inside quoted strings, quotes may be escaped 55 | // with a preceding \ character. No other chars 56 | // may be escaped. The rest of the line is skipped 57 | // if a "#" character is read in. Returns true if 58 | // a token was loaded; false otherwise. 59 | func (l *lexer) next() bool { 60 | var val []rune 61 | var comment, quoted, escaped bool 62 | 63 | makeToken := func() bool { 64 | l.token.Text = string(val) 65 | return true 66 | } 67 | 68 | for { 69 | ch, _, err := l.reader.ReadRune() 70 | if err != nil { 71 | if len(val) > 0 { 72 | return makeToken() 73 | } 74 | if err == io.EOF { 75 | return false 76 | } 77 | panic(err) 78 | } 79 | 80 | if quoted { 81 | if !escaped { 82 | if ch == '\\' { 83 | escaped = true 84 | continue 85 | } else if ch == '"' { 86 | quoted = false 87 | return makeToken() 88 | } 89 | } 90 | if ch == '\n' { 91 | l.line++ 92 | } 93 | if escaped { 94 | // only escape quotes 95 | if ch != '"' { 96 | val = append(val, '\\') 97 | } 98 | } 99 | val = append(val, ch) 100 | escaped = false 101 | continue 102 | } 103 | 104 | if unicode.IsSpace(ch) { 105 | if ch == '\r' { 106 | continue 107 | } 108 | if ch == '\n' { 109 | l.line++ 110 | comment = false 111 | } 112 | if len(val) > 0 { 113 | return makeToken() 114 | } 115 | continue 116 | } 117 | 118 | if ch == '#' { 119 | comment = true 120 | } 121 | 122 | if comment { 123 | continue 124 | } 125 | 126 | if len(val) == 0 { 127 | l.token = Token{Line: l.line} 128 | if ch == '"' { 129 | quoted = true 130 | continue 131 | } 132 | } 133 | 134 | val = append(val, ch) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /caddyhttp/gzip/responsefilter_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/mholt/caddy/caddyhttp/httpserver" 11 | ) 12 | 13 | func TestLengthFilter(t *testing.T) { 14 | var filters = []ResponseFilter{ 15 | LengthFilter(100), 16 | LengthFilter(1000), 17 | LengthFilter(0), 18 | } 19 | 20 | var tests = []struct { 21 | length int64 22 | shouldCompress [3]bool 23 | }{ 24 | {20, [3]bool{false, false, false}}, 25 | {50, [3]bool{false, false, false}}, 26 | {100, [3]bool{true, false, false}}, 27 | {500, [3]bool{true, false, false}}, 28 | {1000, [3]bool{true, true, false}}, 29 | {1500, [3]bool{true, true, false}}, 30 | } 31 | 32 | for i, ts := range tests { 33 | for j, filter := range filters { 34 | r := httptest.NewRecorder() 35 | r.Header().Set("Content-Length", fmt.Sprint(ts.length)) 36 | wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), r, false}) 37 | if filter.ShouldCompress(wWriter) != ts.shouldCompress[j] { 38 | t.Errorf("Test %v: Expected %v found %v", i, ts.shouldCompress[j], filter.ShouldCompress(r)) 39 | } 40 | } 41 | } 42 | } 43 | 44 | func TestResponseFilterWriter(t *testing.T) { 45 | tests := []struct { 46 | body string 47 | shouldCompress bool 48 | }{ 49 | {"Hello\t\t\t\n", false}, 50 | {"Hello the \t\t\t world is\n\n\n great", true}, 51 | {"Hello \t\t\nfrom gzip", true}, 52 | {"Hello gzip\n", false}, 53 | } 54 | 55 | filters := []ResponseFilter{ 56 | LengthFilter(15), 57 | } 58 | 59 | server := Gzip{Configs: []Config{ 60 | {ResponseFilters: filters}, 61 | }} 62 | 63 | for i, ts := range tests { 64 | server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 65 | w.Header().Set("Content-Length", fmt.Sprint(len(ts.body))) 66 | w.Write([]byte(ts.body)) 67 | return 200, nil 68 | }) 69 | 70 | r := urlRequest("/") 71 | r.Header.Set("Accept-Encoding", "gzip") 72 | 73 | w := httptest.NewRecorder() 74 | 75 | server.ServeHTTP(w, r) 76 | 77 | resp := w.Body.String() 78 | 79 | if !ts.shouldCompress { 80 | if resp != ts.body { 81 | t.Errorf("Test %v: No compression expected, found %v", i, resp) 82 | } 83 | } else { 84 | if resp == ts.body { 85 | t.Errorf("Test %v: Compression expected, found %v", i, resp) 86 | } 87 | } 88 | } 89 | } 90 | 91 | func TestResponseGzippedOutput(t *testing.T) { 92 | server := Gzip{Configs: []Config{ 93 | {ResponseFilters: []ResponseFilter{SkipCompressedFilter{}}}, 94 | }} 95 | 96 | server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 97 | w.Header().Set("Content-Encoding", "gzip") 98 | w.Write([]byte("gzipped")) 99 | return 200, nil 100 | }) 101 | 102 | r := urlRequest("/") 103 | r.Header.Set("Accept-Encoding", "gzip") 104 | 105 | w := httptest.NewRecorder() 106 | server.ServeHTTP(w, r) 107 | resp := w.Body.String() 108 | 109 | if resp != "gzipped" { 110 | t.Errorf("Expected output not to be gzipped") 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /caddyhttp/gzip/setup_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | c := caddy.NewTestController("http", `gzip`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, but got: %v", err) 15 | } 16 | mids := httpserver.GetConfig(c).Middleware() 17 | if mids == nil { 18 | t.Fatal("Expected middleware, was nil instead") 19 | } 20 | 21 | handler := mids[0](httpserver.EmptyNext) 22 | myHandler, ok := handler.(Gzip) 23 | if !ok { 24 | t.Fatalf("Expected handler to be type Gzip, got: %#v", handler) 25 | } 26 | 27 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 28 | t.Error("'Next' field of handler was not set properly") 29 | } 30 | 31 | tests := []struct { 32 | input string 33 | shouldErr bool 34 | }{ 35 | {`gzip {`, true}, 36 | {`gzip {}`, true}, 37 | {`gzip a b`, true}, 38 | {`gzip a {`, true}, 39 | {`gzip { not f } `, true}, 40 | {`gzip { not } `, true}, 41 | {`gzip { not /file 42 | ext .html 43 | level 1 44 | } `, false}, 45 | {`gzip { level 9 } `, false}, 46 | {`gzip { ext } `, true}, 47 | {`gzip { ext /f 48 | } `, true}, 49 | {`gzip { not /file 50 | ext .html 51 | level 1 52 | } 53 | gzip`, false}, 54 | {`gzip { 55 | ext "" 56 | }`, false}, 57 | {`gzip { not /file 58 | ext .html 59 | level 1 60 | } 61 | gzip { not /file1 62 | ext .htm 63 | level 3 64 | } 65 | `, false}, 66 | {`gzip { not /file 67 | ext .html 68 | level 1 69 | } 70 | gzip { not /file1 71 | ext .htm 72 | level 3 73 | } 74 | `, false}, 75 | {`gzip { not /file 76 | ext * 77 | level 1 78 | } 79 | `, false}, 80 | {`gzip { not /file 81 | ext * 82 | level 1 83 | min_length ab 84 | } 85 | `, true}, 86 | {`gzip { not /file 87 | ext * 88 | level 1 89 | min_length 1000 90 | } 91 | `, false}, 92 | } 93 | for i, test := range tests { 94 | _, err := gzipParse(caddy.NewTestController("http", test.input)) 95 | if test.shouldErr && err == nil { 96 | t.Errorf("Test %v: Expected error but found nil", i) 97 | } else if !test.shouldErr && err != nil { 98 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 99 | } 100 | } 101 | } 102 | 103 | func TestShouldAddResponseFilters(t *testing.T) { 104 | configs, err := gzipParse(caddy.NewTestController("http", `gzip { min_length 654 }`)) 105 | 106 | if err != nil { 107 | t.Errorf("Test expected no error but found: %v", err) 108 | } 109 | filters := 0 110 | 111 | for _, config := range configs { 112 | for _, filter := range config.ResponseFilters { 113 | switch filter.(type) { 114 | case SkipCompressedFilter: 115 | filters++ 116 | case LengthFilter: 117 | filters++ 118 | 119 | if filter != LengthFilter(654) { 120 | t.Errorf("Expected LengthFilter to have length 654, got: %v", filter) 121 | } 122 | } 123 | } 124 | 125 | if filters != 2 { 126 | t.Errorf("Expected 2 response filters to be registered, got: %v", filters) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /caddyhttp/internalsrv/internal.go: -------------------------------------------------------------------------------- 1 | // Package internalsrv provides a simple middleware that (a) prevents access 2 | // to internal locations and (b) allows to return files from internal location 3 | // by setting a special header, e.g. in a proxy response. 4 | // 5 | // The package is named internalsrv so as not to conflict with Go tooling 6 | // convention which treats folders called "internal" differently. 7 | package internalsrv 8 | 9 | import ( 10 | "net/http" 11 | 12 | "github.com/mholt/caddy/caddyhttp/httpserver" 13 | ) 14 | 15 | // Internal middleware protects internal locations from external requests - 16 | // but allows access from the inside by using a special HTTP header. 17 | type Internal struct { 18 | Next httpserver.Handler 19 | Paths []string 20 | } 21 | 22 | const ( 23 | redirectHeader string = "X-Accel-Redirect" 24 | contentLengthHeader string = "Content-Length" 25 | contentEncodingHeader string = "Content-Encoding" 26 | maxRedirectCount int = 10 27 | ) 28 | 29 | func isInternalRedirect(w http.ResponseWriter) bool { 30 | return w.Header().Get(redirectHeader) != "" 31 | } 32 | 33 | // ServeHTTP implements the httpserver.Handler interface. 34 | func (i Internal) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 35 | 36 | // Internal location requested? -> Not found. 37 | for _, prefix := range i.Paths { 38 | if httpserver.Path(r.URL.Path).Matches(prefix) { 39 | return http.StatusNotFound, nil 40 | } 41 | } 42 | 43 | // Use internal response writer to ignore responses that will be 44 | // redirected to internal locations 45 | iw := internalResponseWriter{ResponseWriter: w} 46 | status, err := i.Next.ServeHTTP(iw, r) 47 | 48 | for c := 0; c < maxRedirectCount && isInternalRedirect(iw); c++ { 49 | // Redirect - adapt request URL path and send it again 50 | // "down the chain" 51 | r.URL.Path = iw.Header().Get(redirectHeader) 52 | iw.ClearHeader() 53 | 54 | status, err = i.Next.ServeHTTP(iw, r) 55 | } 56 | 57 | if isInternalRedirect(iw) { 58 | // Too many redirect cycles 59 | iw.ClearHeader() 60 | return http.StatusInternalServerError, nil 61 | } 62 | 63 | return status, err 64 | } 65 | 66 | // internalResponseWriter wraps the underlying http.ResponseWriter and ignores 67 | // calls to Write and WriteHeader if the response should be redirected to an 68 | // internal location. 69 | type internalResponseWriter struct { 70 | http.ResponseWriter 71 | } 72 | 73 | // ClearHeader removes script headers that would interfere with follow up 74 | // redirect requests. 75 | func (w internalResponseWriter) ClearHeader() { 76 | w.Header().Del(redirectHeader) 77 | w.Header().Del(contentLengthHeader) 78 | w.Header().Del(contentEncodingHeader) 79 | } 80 | 81 | // WriteHeader ignores the call if the response should be redirected to an 82 | // internal location. 83 | func (w internalResponseWriter) WriteHeader(code int) { 84 | if !isInternalRedirect(w) { 85 | w.ResponseWriter.WriteHeader(code) 86 | } 87 | } 88 | 89 | // Write ignores the call if the response should be redirected to an internal 90 | // location. 91 | func (w internalResponseWriter) Write(b []byte) (int, error) { 92 | if isInternalRedirect(w) { 93 | return 0, nil 94 | } 95 | return w.ResponseWriter.Write(b) 96 | } 97 | -------------------------------------------------------------------------------- /caddyhttp/websocket/setup_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestWebSocket(t *testing.T) { 11 | c := caddy.NewTestController("http", `websocket cat`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, got: %v", err) 15 | } 16 | mids := httpserver.GetConfig(c).Middleware() 17 | if len(mids) == 0 { 18 | t.Fatal("Expected middleware, got 0 instead") 19 | } 20 | 21 | handler := mids[0](httpserver.EmptyNext) 22 | myHandler, ok := handler.(WebSocket) 23 | 24 | if !ok { 25 | t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler) 26 | } 27 | 28 | if myHandler.Sockets[0].Path != "/" { 29 | t.Errorf("Expected / as the default Path") 30 | } 31 | if myHandler.Sockets[0].Command != "cat" { 32 | t.Errorf("Expected %s as the command", "cat") 33 | } 34 | 35 | } 36 | func TestWebSocketParse(t *testing.T) { 37 | tests := []struct { 38 | inputWebSocketConfig string 39 | shouldErr bool 40 | expectedWebSocketConfig []Config 41 | }{ 42 | {`websocket /api1 cat`, false, []Config{{ 43 | Path: "/api1", 44 | Command: "cat", 45 | }}}, 46 | 47 | {`websocket /api3 cat 48 | websocket /api4 cat `, false, []Config{{ 49 | Path: "/api3", 50 | Command: "cat", 51 | }, { 52 | Path: "/api4", 53 | Command: "cat", 54 | }}}, 55 | 56 | {`websocket /api5 "cmd arg1 arg2 arg3"`, false, []Config{{ 57 | Path: "/api5", 58 | Command: "cmd", 59 | Arguments: []string{"arg1", "arg2", "arg3"}, 60 | }}}, 61 | 62 | // accept respawn 63 | {`websocket /api6 cat { 64 | respawn 65 | }`, false, []Config{{ 66 | Path: "/api6", 67 | Command: "cat", 68 | }}}, 69 | 70 | // invalid configuration 71 | {`websocket /api7 cat { 72 | invalid 73 | }`, true, []Config{}}, 74 | } 75 | for i, test := range tests { 76 | c := caddy.NewTestController("http", test.inputWebSocketConfig) 77 | actualWebSocketConfigs, err := webSocketParse(c) 78 | 79 | if err == nil && test.shouldErr { 80 | t.Errorf("Test %d didn't error, but it should have", i) 81 | } else if err != nil && !test.shouldErr { 82 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 83 | } 84 | if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) { 85 | t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ", 86 | i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs)) 87 | } 88 | for j, actualWebSocketConfig := range actualWebSocketConfigs { 89 | 90 | if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path { 91 | t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s", 92 | i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path) 93 | } 94 | 95 | if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command { 96 | t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s", 97 | i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command) 98 | } 99 | 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /caddyhttp/templates/templates.go: -------------------------------------------------------------------------------- 1 | // Package templates implements template execution for files to be 2 | // dynamically rendered for the client. 3 | package templates 4 | 5 | import ( 6 | "bytes" 7 | "mime" 8 | "net/http" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "text/template" 13 | 14 | "github.com/mholt/caddy/caddyhttp/httpserver" 15 | ) 16 | 17 | // ServeHTTP implements the httpserver.Handler interface. 18 | func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 19 | for _, rule := range t.Rules { 20 | if !httpserver.Path(r.URL.Path).Matches(rule.Path) { 21 | continue 22 | } 23 | 24 | // Check for index files 25 | fpath := r.URL.Path 26 | if idx, ok := httpserver.IndexFile(t.FileSys, fpath, rule.IndexFiles); ok { 27 | fpath = idx 28 | } 29 | 30 | // Check the extension 31 | reqExt := path.Ext(fpath) 32 | 33 | for _, ext := range rule.Extensions { 34 | if reqExt == ext { 35 | // Create execution context 36 | ctx := httpserver.Context{Root: t.FileSys, Req: r, URL: r.URL} 37 | 38 | // New template 39 | templateName := filepath.Base(fpath) 40 | tpl := template.New(templateName) 41 | 42 | // Set delims 43 | if rule.Delims != [2]string{} { 44 | tpl.Delims(rule.Delims[0], rule.Delims[1]) 45 | } 46 | 47 | // Build the template 48 | templatePath := filepath.Join(t.Root, fpath) 49 | tpl, err := tpl.ParseFiles(templatePath) 50 | if err != nil { 51 | if os.IsNotExist(err) { 52 | return http.StatusNotFound, nil 53 | } else if os.IsPermission(err) { 54 | return http.StatusForbidden, nil 55 | } 56 | return http.StatusInternalServerError, err 57 | } 58 | 59 | // Execute it 60 | var buf bytes.Buffer 61 | err = tpl.Execute(&buf, ctx) 62 | if err != nil { 63 | return http.StatusInternalServerError, err 64 | } 65 | 66 | // If Content-Type isn't set here, http.ResponseWriter.Write 67 | // will set it according to response body. But other middleware 68 | // such as gzip can modify response body, then Content-Type 69 | // detected by http.ResponseWriter.Write is wrong. 70 | ctype := mime.TypeByExtension(ext) 71 | if ctype == "" { 72 | ctype = http.DetectContentType(buf.Bytes()) 73 | } 74 | w.Header().Set("Content-Type", ctype) 75 | 76 | templateInfo, err := os.Stat(templatePath) 77 | if err == nil { 78 | // add the Last-Modified header if we were able to read the stamp 79 | httpserver.SetLastModifiedHeader(w, templateInfo.ModTime()) 80 | } 81 | buf.WriteTo(w) 82 | 83 | return http.StatusOK, nil 84 | } 85 | } 86 | } 87 | 88 | return t.Next.ServeHTTP(w, r) 89 | } 90 | 91 | // Templates is middleware to render templated files as the HTTP response. 92 | type Templates struct { 93 | Next httpserver.Handler 94 | Rules []Rule 95 | Root string 96 | FileSys http.FileSystem 97 | } 98 | 99 | // Rule represents a template rule. A template will only execute 100 | // with this rule if the request path matches the Path specified 101 | // and requests a resource with one of the extensions specified. 102 | type Rule struct { 103 | Path string 104 | Extensions []string 105 | IndexFiles []string 106 | Delims [2]string 107 | } 108 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "errors" 5 | "runtime" 6 | "unicode" 7 | 8 | "github.com/flynn/go-shlex" 9 | ) 10 | 11 | var runtimeGoos = runtime.GOOS 12 | 13 | // SplitCommandAndArgs takes a command string and parses it shell-style into the 14 | // command and its separate arguments. 15 | func SplitCommandAndArgs(command string) (cmd string, args []string, err error) { 16 | var parts []string 17 | 18 | if runtimeGoos == "windows" { 19 | parts = parseWindowsCommand(command) // parse it Windows-style 20 | } else { 21 | parts, err = parseUnixCommand(command) // parse it Unix-style 22 | if err != nil { 23 | err = errors.New("error parsing command: " + err.Error()) 24 | return 25 | } 26 | } 27 | 28 | if len(parts) == 0 { 29 | err = errors.New("no command contained in '" + command + "'") 30 | return 31 | } 32 | 33 | cmd = parts[0] 34 | if len(parts) > 1 { 35 | args = parts[1:] 36 | } 37 | 38 | return 39 | } 40 | 41 | // parseUnixCommand parses a unix style command line and returns the 42 | // command and its arguments or an error 43 | func parseUnixCommand(cmd string) ([]string, error) { 44 | return shlex.Split(cmd) 45 | } 46 | 47 | // parseWindowsCommand parses windows command lines and 48 | // returns the command and the arguments as an array. It 49 | // should be able to parse commonly used command lines. 50 | // Only basic syntax is supported: 51 | // - spaces in double quotes are not token delimiters 52 | // - double quotes are escaped by either backspace or another double quote 53 | // - except for the above case backspaces are path separators (not special) 54 | // 55 | // Many sources point out that escaping quotes using backslash can be unsafe. 56 | // Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 ) 57 | // 58 | // This function has to be used on Windows instead 59 | // of the shlex package because this function treats backslash 60 | // characters properly. 61 | func parseWindowsCommand(cmd string) []string { 62 | const backslash = '\\' 63 | const quote = '"' 64 | 65 | var parts []string 66 | var part string 67 | var inQuotes bool 68 | var lastRune rune 69 | 70 | for i, ch := range cmd { 71 | 72 | if i != 0 { 73 | lastRune = rune(cmd[i-1]) 74 | } 75 | 76 | if ch == backslash { 77 | // put it in the part - for now we don't know if it's an 78 | // escaping char or path separator 79 | part += string(ch) 80 | continue 81 | } 82 | 83 | if ch == quote { 84 | if lastRune == backslash { 85 | // remove the backslash from the part and add the escaped quote instead 86 | part = part[:len(part)-1] 87 | part += string(ch) 88 | continue 89 | } 90 | 91 | if lastRune == quote { 92 | // revert the last change of the inQuotes state 93 | // it was an escaping quote 94 | inQuotes = !inQuotes 95 | part += string(ch) 96 | continue 97 | } 98 | 99 | // normal escaping quotes 100 | inQuotes = !inQuotes 101 | continue 102 | 103 | } 104 | 105 | if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 { 106 | parts = append(parts, part) 107 | part = "" 108 | continue 109 | } 110 | 111 | part += string(ch) 112 | } 113 | 114 | if len(part) > 0 { 115 | parts = append(parts, part) 116 | } 117 | 118 | return parts 119 | } 120 | -------------------------------------------------------------------------------- /caddyhttp/status/setup_test.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy" 7 | "github.com/mholt/caddy/caddyhttp/httpserver" 8 | ) 9 | 10 | func TestSetup(t *testing.T) { 11 | c := caddy.NewTestController("http", `status 404 /foo`) 12 | err := setup(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, but got: %v", err) 15 | } 16 | 17 | mids := httpserver.GetConfig(c).Middleware() 18 | if len(mids) == 0 { 19 | t.Fatal("Expected middleware, had 0 instead") 20 | } 21 | 22 | handler := mids[0](httpserver.EmptyNext) 23 | myHandler, ok := handler.(Status) 24 | if !ok { 25 | t.Fatalf("Expected handler to be type Status, got: %#v", 26 | handler) 27 | } 28 | 29 | if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { 30 | t.Error("'Next' field of handler was not set properly") 31 | } 32 | 33 | if len(myHandler.Rules) != 1 { 34 | t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules)) 35 | } 36 | } 37 | 38 | func TestStatusParse(t *testing.T) { 39 | tests := []struct { 40 | input string 41 | shouldErr bool 42 | expected []*Rule 43 | }{ 44 | {`status`, true, []*Rule{}}, 45 | {`status /foo`, true, []*Rule{}}, 46 | {`status bar /foo`, true, []*Rule{}}, 47 | {`status 404 /foo bar`, true, []*Rule{}}, 48 | {`status 404 /foo`, false, []*Rule{ 49 | {Base: "/foo", StatusCode: 404}, 50 | }, 51 | }, 52 | {`status { 53 | }`, 54 | true, 55 | []*Rule{}, 56 | }, 57 | {`status 404 { 58 | }`, 59 | true, 60 | []*Rule{}, 61 | }, 62 | {`status 404 { 63 | /foo 64 | /foo 65 | }`, 66 | true, 67 | []*Rule{}, 68 | }, 69 | {`status 404 { 70 | 404 /foo 71 | }`, 72 | true, 73 | []*Rule{}, 74 | }, 75 | {`status 404 { 76 | /foo 77 | /bar 78 | }`, 79 | false, 80 | []*Rule{ 81 | {Base: "/foo", StatusCode: 404}, 82 | {Base: "/bar", StatusCode: 404}, 83 | }, 84 | }, 85 | } 86 | 87 | for i, test := range tests { 88 | actual, err := statusParse(caddy.NewTestController("http", 89 | test.input)) 90 | 91 | if err == nil && test.shouldErr { 92 | t.Errorf("Test %d didn't error, but it should have", i) 93 | } else if err != nil && !test.shouldErr { 94 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", 95 | i, err) 96 | } else if err != nil && test.shouldErr { 97 | continue 98 | } 99 | 100 | if len(actual) != len(test.expected) { 101 | t.Fatalf("Test %d expected %d rules, but got %d", 102 | i, len(test.expected), len(actual)) 103 | } 104 | 105 | findRule := func(basePath string) (bool, *Rule) { 106 | for _, cfg := range actual { 107 | actualRule := cfg.(*Rule) 108 | 109 | if actualRule.Base == basePath { 110 | return true, actualRule 111 | } 112 | } 113 | 114 | return false, nil 115 | } 116 | 117 | for _, expectedRule := range test.expected { 118 | found, actualRule := findRule(expectedRule.Base) 119 | 120 | if !found { 121 | t.Errorf("Test %d: Missing rule for path '%s'", 122 | i, expectedRule.Base) 123 | } 124 | 125 | if actualRule.StatusCode != expectedRule.StatusCode { 126 | t.Errorf("Test %d: Expected status code %d for path '%s'. Got %d", 127 | i, expectedRule.StatusCode, expectedRule.Base, actualRule.StatusCode) 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /caddyhttp/gzip/gzip_test.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/mholt/caddy/caddyhttp/httpserver" 12 | ) 13 | 14 | func TestGzipHandler(t *testing.T) { 15 | pathFilter := PathFilter{make(Set)} 16 | badPaths := []string{"/bad", "/nogzip", "/nongzip"} 17 | for _, p := range badPaths { 18 | pathFilter.IgnoredPaths.Add(p) 19 | } 20 | extFilter := ExtFilter{make(Set)} 21 | for _, e := range []string{".txt", ".html", ".css", ".md"} { 22 | extFilter.Exts.Add(e) 23 | } 24 | gz := Gzip{Configs: []Config{ 25 | {RequestFilters: []RequestFilter{pathFilter, extFilter}}, 26 | }} 27 | 28 | w := httptest.NewRecorder() 29 | gz.Next = nextFunc(true) 30 | var exts = []string{ 31 | ".html", ".css", ".md", 32 | } 33 | for _, e := range exts { 34 | url := "/file" + e 35 | r, err := http.NewRequest("GET", url, nil) 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | r.Header.Set("Accept-Encoding", "gzip") 40 | _, err = gz.ServeHTTP(w, r) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | } 45 | 46 | w = httptest.NewRecorder() 47 | gz.Next = nextFunc(false) 48 | for _, p := range badPaths { 49 | for _, e := range exts { 50 | url := p + "/file" + e 51 | r, err := http.NewRequest("GET", url, nil) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | r.Header.Set("Accept-Encoding", "gzip") 56 | _, err = gz.ServeHTTP(w, r) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | } 61 | } 62 | 63 | w = httptest.NewRecorder() 64 | gz.Next = nextFunc(false) 65 | exts = []string{ 66 | ".htm1", ".abc", ".mdx", 67 | } 68 | for _, e := range exts { 69 | url := "/file" + e 70 | r, err := http.NewRequest("GET", url, nil) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | r.Header.Set("Accept-Encoding", "gzip") 75 | _, err = gz.ServeHTTP(w, r) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | } 80 | } 81 | 82 | func nextFunc(shouldGzip bool) httpserver.Handler { 83 | return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 84 | // write a relatively large text file 85 | b, err := ioutil.ReadFile("testdata/test.txt") 86 | if err != nil { 87 | return 500, err 88 | } 89 | if _, err := w.Write(b); err != nil { 90 | return 500, err 91 | } 92 | 93 | if shouldGzip { 94 | if w.Header().Get("Content-Encoding") != "gzip" { 95 | return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", r.Header.Get("Content-Encoding")) 96 | } 97 | if w.Header().Get("Vary") != "Accept-Encoding" { 98 | return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", r.Header.Get("Vary")) 99 | } 100 | if _, ok := w.(*gzipResponseWriter); !ok { 101 | return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w) 102 | } 103 | if strings.Contains(w.Header().Get("Content-Type"), "application/x-gzip") { 104 | return 0, fmt.Errorf("Content-Type should not be gzip") 105 | } 106 | return 0, nil 107 | } 108 | if r.Header.Get("Accept-Encoding") == "" { 109 | return 0, fmt.Errorf("Accept-Encoding header expected") 110 | } 111 | if w.Header().Get("Content-Encoding") == "gzip" { 112 | return 0, fmt.Errorf("Content-Encoding must not be gzip, found gzip") 113 | } 114 | if _, ok := w.(*gzipResponseWriter); ok { 115 | return 0, fmt.Errorf("ResponseWriter should not be gzipResponseWriter") 116 | } 117 | return 0, nil 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /caddyhttp/fastcgi/dialer_test.go: -------------------------------------------------------------------------------- 1 | package fastcgi 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestLoadbalancingDialer(t *testing.T) { 9 | // given 10 | runs := 100 11 | mockDialer1 := new(mockDialer) 12 | mockDialer2 := new(mockDialer) 13 | 14 | dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}} 15 | 16 | // when 17 | for i := 0; i < runs; i++ { 18 | client, err := dialer.Dial() 19 | dialer.Close(client) 20 | 21 | if err != nil { 22 | t.Errorf("Expected error to be nil") 23 | } 24 | } 25 | 26 | // then 27 | if mockDialer1.dialCalled != mockDialer2.dialCalled && mockDialer1.dialCalled != 50 { 28 | t.Errorf("Expected dialer to call Dial() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.dialCalled, mockDialer2.dialCalled) 29 | } 30 | 31 | if mockDialer1.closeCalled != mockDialer2.closeCalled && mockDialer1.closeCalled != 50 { 32 | t.Errorf("Expected dialer to call Close() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.closeCalled, mockDialer2.closeCalled) 33 | } 34 | } 35 | 36 | func TestLoadBalancingDialerShouldReturnDialerAwareClient(t *testing.T) { 37 | // given 38 | mockDialer1 := new(mockDialer) 39 | dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}} 40 | 41 | // when 42 | client, err := dialer.Dial() 43 | 44 | // then 45 | if err != nil { 46 | t.Errorf("Expected error to be nil") 47 | } 48 | 49 | if awareClient, ok := client.(*dialerAwareClient); !ok { 50 | t.Error("Expected dialer to wrap client") 51 | } else { 52 | if awareClient.dialer != mockDialer1 { 53 | t.Error("Expected wrapped client to have reference to dialer") 54 | } 55 | } 56 | } 57 | 58 | func TestLoadBalancingDialerShouldUnderlyingReturnDialerError(t *testing.T) { 59 | // given 60 | mockDialer1 := new(errorReturningDialer) 61 | dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}} 62 | 63 | // when 64 | _, err := dialer.Dial() 65 | 66 | // then 67 | if err.Error() != "Error during dial" { 68 | t.Errorf("Expected 'Error during dial', got: '%s'", err.Error()) 69 | } 70 | } 71 | 72 | func TestLoadBalancingDialerShouldCloseClient(t *testing.T) { 73 | // given 74 | mockDialer1 := new(mockDialer) 75 | mockDialer2 := new(mockDialer) 76 | 77 | dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}} 78 | client, _ := dialer.Dial() 79 | 80 | // when 81 | err := dialer.Close(client) 82 | 83 | // then 84 | if err != nil { 85 | t.Error("Expected error not to occur") 86 | } 87 | 88 | // load balancing starts from index 1 89 | if mockDialer2.client != client { 90 | t.Errorf("Expected Close() to be called on referenced dialer") 91 | } 92 | } 93 | 94 | type mockDialer struct { 95 | dialCalled int 96 | closeCalled int 97 | client Client 98 | } 99 | 100 | type mockClient struct { 101 | Client 102 | } 103 | 104 | func (m *mockDialer) Dial() (Client, error) { 105 | m.dialCalled++ 106 | return mockClient{Client: &FCGIClient{}}, nil 107 | } 108 | 109 | func (m *mockDialer) Close(c Client) error { 110 | m.client = c 111 | m.closeCalled++ 112 | return nil 113 | } 114 | 115 | type errorReturningDialer struct { 116 | client Client 117 | } 118 | 119 | func (m *errorReturningDialer) Dial() (Client, error) { 120 | return mockClient{Client: &FCGIClient{}}, errors.New("Error during dial") 121 | } 122 | 123 | func (m *errorReturningDialer) Close(c Client) error { 124 | m.client = c 125 | return errors.New("Error during close") 126 | } 127 | -------------------------------------------------------------------------------- /caddyhttp/gzip/setup.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/mholt/caddy" 9 | "github.com/mholt/caddy/caddyhttp/httpserver" 10 | ) 11 | 12 | // setup configures a new gzip middleware instance. 13 | func setup(c *caddy.Controller) error { 14 | configs, err := gzipParse(c) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { 20 | return Gzip{Next: next, Configs: configs} 21 | }) 22 | 23 | return nil 24 | } 25 | 26 | func gzipParse(c *caddy.Controller) ([]Config, error) { 27 | var configs []Config 28 | 29 | for c.Next() { 30 | config := Config{} 31 | 32 | // Request Filters 33 | pathFilter := PathFilter{IgnoredPaths: make(Set)} 34 | extFilter := ExtFilter{Exts: make(Set)} 35 | 36 | // Response Filters 37 | lengthFilter := LengthFilter(0) 38 | 39 | // No extra args expected 40 | if len(c.RemainingArgs()) > 0 { 41 | return configs, c.ArgErr() 42 | } 43 | 44 | for c.NextBlock() { 45 | switch c.Val() { 46 | case "ext": 47 | exts := c.RemainingArgs() 48 | if len(exts) == 0 { 49 | return configs, c.ArgErr() 50 | } 51 | for _, e := range exts { 52 | if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" { 53 | return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e) 54 | } 55 | extFilter.Exts.Add(e) 56 | } 57 | case "not": 58 | paths := c.RemainingArgs() 59 | if len(paths) == 0 { 60 | return configs, c.ArgErr() 61 | } 62 | for _, p := range paths { 63 | if p == "/" { 64 | return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`) 65 | } 66 | if !strings.HasPrefix(p, "/") { 67 | return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p) 68 | } 69 | pathFilter.IgnoredPaths.Add(p) 70 | } 71 | case "level": 72 | if !c.NextArg() { 73 | return configs, c.ArgErr() 74 | } 75 | level, _ := strconv.Atoi(c.Val()) 76 | config.Level = level 77 | case "min_length": 78 | if !c.NextArg() { 79 | return configs, c.ArgErr() 80 | } 81 | length, err := strconv.ParseInt(c.Val(), 10, 64) 82 | if err != nil { 83 | return configs, err 84 | } else if length == 0 { 85 | return configs, fmt.Errorf(`gzip: min_length must be greater than 0`) 86 | } 87 | lengthFilter = LengthFilter(length) 88 | default: 89 | return configs, c.ArgErr() 90 | } 91 | } 92 | 93 | // Request Filters 94 | config.RequestFilters = []RequestFilter{} 95 | 96 | // If ignored paths are specified, put in front to filter with path first 97 | if len(pathFilter.IgnoredPaths) > 0 { 98 | config.RequestFilters = []RequestFilter{pathFilter} 99 | } 100 | 101 | // Then, if extensions are specified, use those to filter. 102 | // Otherwise, use default extensions filter. 103 | if len(extFilter.Exts) > 0 { 104 | config.RequestFilters = append(config.RequestFilters, extFilter) 105 | } else { 106 | config.RequestFilters = append(config.RequestFilters, DefaultExtFilter()) 107 | } 108 | 109 | config.ResponseFilters = append(config.ResponseFilters, SkipCompressedFilter{}) 110 | 111 | // Response Filters 112 | // If min_length is specified, use it. 113 | if int64(lengthFilter) != 0 { 114 | config.ResponseFilters = append(config.ResponseFilters, lengthFilter) 115 | } 116 | 117 | configs = append(configs, config) 118 | } 119 | 120 | return configs, nil 121 | } 122 | -------------------------------------------------------------------------------- /caddyhttp/markdown/metadata/metadata.go: -------------------------------------------------------------------------------- 1 | package metadata 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // Date format YYYY-MM-DD HH:MM:SS or YYYY-MM-DD 11 | timeLayout = []string{ 12 | `2006-01-02 15:04:05-0700`, 13 | `2006-01-02 15:04:05`, 14 | `2006-01-02`, 15 | } 16 | ) 17 | 18 | // Metadata stores a page's metadata 19 | type Metadata struct { 20 | // Page title 21 | Title string 22 | 23 | // Page template 24 | Template string 25 | 26 | // Publish date 27 | Date time.Time 28 | 29 | // Variables to be used with Template 30 | Variables map[string]interface{} 31 | } 32 | 33 | // NewMetadata returns a new Metadata struct, loaded with the given map 34 | func NewMetadata(parsedMap map[string]interface{}) Metadata { 35 | md := Metadata{ 36 | Variables: make(map[string]interface{}), 37 | } 38 | md.load(parsedMap) 39 | 40 | return md 41 | } 42 | 43 | // load loads parsed values in parsedMap into Metadata 44 | func (m *Metadata) load(parsedMap map[string]interface{}) { 45 | 46 | // Pull top level things out 47 | if title, ok := parsedMap["title"]; ok { 48 | m.Title, _ = title.(string) 49 | } 50 | if template, ok := parsedMap["template"]; ok { 51 | m.Template, _ = template.(string) 52 | } 53 | if date, ok := parsedMap["date"].(string); ok { 54 | for _, layout := range timeLayout { 55 | if t, err := time.Parse(layout, date); err == nil { 56 | m.Date = t 57 | break 58 | } 59 | } 60 | } 61 | 62 | m.Variables = parsedMap 63 | } 64 | 65 | // Parser is a an interface that must be satisfied by each parser 66 | type Parser interface { 67 | // Initialize a parser 68 | Init(b *bytes.Buffer) bool 69 | 70 | // Type of metadata 71 | Type() string 72 | 73 | // Parsed metadata. 74 | Metadata() Metadata 75 | 76 | // Raw markdown. 77 | Markdown() []byte 78 | } 79 | 80 | // GetParser returns a parser for the given data 81 | func GetParser(buf []byte) Parser { 82 | for _, p := range parsers() { 83 | b := bytes.NewBuffer(buf) 84 | if p.Init(b) { 85 | return p 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // parsers returns all available parsers 93 | func parsers() []Parser { 94 | return []Parser{ 95 | &TOMLParser{}, 96 | &YAMLParser{}, 97 | &JSONParser{}, 98 | 99 | // This one must be last 100 | &NoneParser{}, 101 | } 102 | } 103 | 104 | // Split out prefixed/suffixed metadata with given delimiter 105 | func splitBuffer(b *bytes.Buffer, delim string) (*bytes.Buffer, *bytes.Buffer) { 106 | scanner := bufio.NewScanner(b) 107 | 108 | // Read and check first line 109 | if !scanner.Scan() { 110 | return nil, nil 111 | } 112 | if string(bytes.TrimSpace(scanner.Bytes())) != delim { 113 | return nil, nil 114 | } 115 | 116 | // Accumulate metadata, until delimiter 117 | meta := bytes.NewBuffer(nil) 118 | for scanner.Scan() { 119 | if string(bytes.TrimSpace(scanner.Bytes())) == delim { 120 | break 121 | } 122 | if _, err := meta.Write(scanner.Bytes()); err != nil { 123 | return nil, nil 124 | } 125 | if _, err := meta.WriteRune('\n'); err != nil { 126 | return nil, nil 127 | } 128 | } 129 | // Make sure we saw closing delimiter 130 | if string(bytes.TrimSpace(scanner.Bytes())) != delim { 131 | return nil, nil 132 | } 133 | 134 | // The rest is markdown 135 | markdown := new(bytes.Buffer) 136 | for scanner.Scan() { 137 | if _, err := markdown.Write(scanner.Bytes()); err != nil { 138 | return nil, nil 139 | } 140 | if _, err := markdown.WriteRune('\n'); err != nil { 141 | return nil, nil 142 | } 143 | } 144 | 145 | return meta, markdown 146 | } 147 | --------------------------------------------------------------------------------