├── middleware ├── rewrite │ ├── testdata │ │ ├── testdir │ │ │ └── empty │ │ └── testfile │ ├── to_test.go │ ├── to.go │ ├── condition_test.go │ └── condition.go ├── browse │ └── testdata │ │ ├── header.html │ │ ├── photos │ │ ├── test3.html │ │ ├── test.html │ │ └── test2.html │ │ └── photos.tpl ├── templates │ ├── testdata │ │ ├── header.html │ │ ├── images │ │ │ ├── header.html │ │ │ ├── img.htm │ │ │ └── img2.htm │ │ ├── root.html │ │ └── photos │ │ │ └── test.html │ └── templates.go ├── markdown │ ├── testdata │ │ ├── header.html │ │ ├── docflags │ │ │ ├── test.md │ │ │ └── template.txt │ │ ├── og │ │ │ └── first.md │ │ ├── blog │ │ │ └── test.md │ │ ├── log │ │ │ └── test.md │ │ └── markdown_tpl.html │ ├── watcher.go │ └── watcher_test.go ├── websocket │ └── websocket_test.go ├── roller.go ├── mime │ ├── mime.go │ └── mime_test.go ├── expvar │ ├── expvar_test.go │ └── expvar.go ├── path.go ├── pprof │ ├── pprof.go │ └── pprof_test.go ├── recorder_test.go ├── headers │ ├── headers_test.go │ └── headers.go ├── extensions │ ├── ext_test.go │ └── ext.go ├── log │ ├── log_test.go │ └── log.go ├── path_test.go ├── inner │ ├── internal_test.go │ └── internal.go ├── redirect │ └── redirect.go ├── fastcgi │ └── fcgi_test.php ├── gzip │ ├── response_filter_test.go │ ├── response_filter.go │ ├── request_filter.go │ ├── request_filter_test.go │ └── gzip_test.go ├── proxy │ ├── policy_test.go │ ├── policy.go │ └── upstream_test.go ├── recorder.go ├── commands.go └── middleware_test.go ├── caddy ├── parse │ ├── import_test1.txt │ ├── import_glob2.txt │ ├── import_test2.txt │ ├── import_glob1.txt │ ├── import_glob0.txt │ ├── parse_test.go │ ├── parse.go │ └── lexer.go ├── setup │ ├── testdata │ │ ├── blog │ │ │ └── first_post.md │ │ ├── header.html │ │ └── tpl_with_include.html │ ├── bindhost.go │ ├── proxy.go │ ├── pprof_test.go │ ├── pprof.go │ ├── internal.go │ ├── root.go │ ├── roller.go │ ├── expvar_test.go │ ├── ext.go │ ├── expvar.go │ ├── mime_test.go │ ├── mime.go │ ├── startupshutdown.go │ ├── headers.go │ ├── startupshutdown_test.go │ ├── basicauth.go │ ├── internal_test.go │ ├── browse_test.go │ ├── ext_test.go │ ├── templates.go │ ├── websocket.go │ ├── gzip_test.go │ ├── headers_test.go │ ├── controller.go │ ├── rewrite.go │ ├── fastcgi.go │ ├── websocket_test.go │ ├── root_test.go │ ├── redir_test.go │ ├── gzip.go │ ├── log.go │ ├── templates_test.go │ └── errors.go ├── sigtrap_windows.go ├── assets │ ├── path_test.go │ └── path.go ├── restart_windows.go ├── caddy_test.go ├── restartinproc.go ├── directives_test.go ├── https │ ├── handler.go │ ├── crypto.go │ ├── handler_test.go │ ├── handshake_test.go │ ├── storage.go │ ├── certificates_test.go │ ├── crypto_test.go │ └── storage_test.go ├── sigtrap.go ├── sigtrap_posix.go └── helpers.go ├── dist ├── gitcookie.sh.enc ├── init │ ├── linux-upstart │ │ ├── caddy.conf │ │ └── README.md │ ├── linux-systemd │ │ ├── caddy@.service │ │ └── README.md │ ├── README.md │ └── freebsd │ │ └── caddy └── README.txt ├── .gitignore ├── appveyor.yml ├── ISSUE_TEMPLATE ├── server ├── config_test.go ├── virtualhost.go ├── server_test.go ├── graceful.go └── config.go ├── .travis.yml ├── .gitattributes └── main_test.go /middleware/rewrite/testdata/testdir/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /middleware/rewrite/testdata/testfile: -------------------------------------------------------------------------------- 1 | empty -------------------------------------------------------------------------------- /caddy/parse/import_test1.txt: -------------------------------------------------------------------------------- 1 | dir2 arg1 arg2 2 | dir3 -------------------------------------------------------------------------------- /caddy/setup/testdata/blog/first_post.md: -------------------------------------------------------------------------------- 1 | # Test h1 2 | -------------------------------------------------------------------------------- /caddy/setup/testdata/header.html: -------------------------------------------------------------------------------- 1 |

Header title

2 | -------------------------------------------------------------------------------- /middleware/browse/testdata/header.html: -------------------------------------------------------------------------------- 1 |

Header

2 | -------------------------------------------------------------------------------- /caddy/parse/import_glob2.txt: -------------------------------------------------------------------------------- 1 | glob2.host0 { 2 | dir2 arg1 3 | } 4 | -------------------------------------------------------------------------------- /caddy/parse/import_test2.txt: -------------------------------------------------------------------------------- 1 | host1 { 2 | dir1 3 | dir2 arg1 4 | } -------------------------------------------------------------------------------- /middleware/templates/testdata/header.html: -------------------------------------------------------------------------------- 1 |

Header title

2 | -------------------------------------------------------------------------------- /caddy/parse/import_glob1.txt: -------------------------------------------------------------------------------- 1 | glob1.host0 { 2 | dir1 3 | dir2 arg1 4 | } 5 | -------------------------------------------------------------------------------- /caddy/sigtrap_windows.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | func trapSignalsPosix() {} 4 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/header.html: -------------------------------------------------------------------------------- 1 |

Header for: {{.Doc.title}}

-------------------------------------------------------------------------------- /middleware/templates/testdata/images/header.html: -------------------------------------------------------------------------------- 1 |

Header title

2 | -------------------------------------------------------------------------------- /middleware/browse/testdata/photos/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dist/gitcookie.sh.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scslab/caddy/master/dist/gitcookie.sh.enc -------------------------------------------------------------------------------- /caddy/parse/import_glob0.txt: -------------------------------------------------------------------------------- 1 | glob0.host0 { 2 | dir2 arg1 3 | } 4 | 5 | glob0.host1 { 6 | } 7 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/docflags/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | var_string: hello 3 | var_bool: true 4 | --- 5 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/og/first.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: first_post 3 | sitename: title 4 | --- 5 | # Test h1 6 | -------------------------------------------------------------------------------- /middleware/templates/testdata/root.html: -------------------------------------------------------------------------------- 1 | root{{.Include "header.html"}} 2 | -------------------------------------------------------------------------------- /middleware/templates/testdata/images/img.htm: -------------------------------------------------------------------------------- 1 | img{%.Include "header.html"%} 2 | -------------------------------------------------------------------------------- /middleware/templates/testdata/images/img2.htm: -------------------------------------------------------------------------------- 1 | img{{.Include "header.html"}} 2 | -------------------------------------------------------------------------------- /middleware/browse/testdata/photos/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /middleware/templates/testdata/photos/test.html: -------------------------------------------------------------------------------- 1 | test page{{.Include "../header.html"}} 2 | -------------------------------------------------------------------------------- /middleware/browse/testdata/photos/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 2 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/docflags/template.txt: -------------------------------------------------------------------------------- 1 | Doc.var_string {{.Doc.var_string}} 2 | Doc.var_bool {{.Doc.var_bool}} 3 | DocFlags.var_string {{.DocFlags.var_string}} 4 | DocFlags.var_bool {{.DocFlags.var_bool}} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | _gitignore/ 4 | Vagrantfile 5 | .vagrant/ 6 | 7 | dist/builds/ 8 | dist/release/ 9 | 10 | error.log 11 | access.log 12 | 13 | /*.conf 14 | Caddyfile 15 | 16 | og_static/ -------------------------------------------------------------------------------- /caddy/setup/testdata/tpl_with_include.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{.Doc.title}} 5 | 6 | 7 | {{.Include "header.html"}} 8 | {{.Doc.body}} 9 | 10 | 11 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/blog/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown test 1 3 | sitename: A Caddy website 4 | --- 5 | 6 | ## Welcome on the blog 7 | 8 | Body 9 | 10 | ``` go 11 | func getTrue() bool { 12 | return true 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/log/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown test 2 3 | sitename: A Caddy website 4 | --- 5 | 6 | ## Welcome on the blog 7 | 8 | Body 9 | 10 | ``` go 11 | func getTrue() bool { 12 | return true 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /middleware/markdown/testdata/markdown_tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{.Doc.title}} 5 | 6 | 7 | {{.Include "header.html"}} 8 | Welcome to {{.Doc.sitename}}! 9 | {{.Doc.body}} 10 | 11 | 12 | -------------------------------------------------------------------------------- /middleware/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 | -------------------------------------------------------------------------------- /caddy/assets/path_test.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestPath(t *testing.T) { 9 | if actual := Path(); !strings.HasSuffix(actual, ".caddy") { 10 | t.Errorf("Expected path to be a .caddy folder, got: %v", actual) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /caddy/setup/bindhost.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import "github.com/mholt/caddy/middleware" 4 | 5 | // BindHost sets the host to bind the listener to. 6 | func BindHost(c *Controller) (middleware.Middleware, error) { 7 | for c.Next() { 8 | if !c.Args(&c.BindHost) { 9 | return nil, c.ArgErr() 10 | } 11 | } 12 | return nil, nil 13 | } 14 | -------------------------------------------------------------------------------- /dist/init/linux-upstart/caddy.conf: -------------------------------------------------------------------------------- 1 | description "Caddy startup script" 2 | author "Mathias Beke" 3 | 4 | start on runlevel [2345] 5 | stop on runlevel [016] 6 | 7 | 8 | setuid www-data 9 | setgid www-data 10 | 11 | respawn 12 | respawn limit 10 5 13 | 14 | limit nofile 4096 4096 15 | 16 | script 17 | exec /usr/bin/caddy -agree=true -conf=/etc/caddy/Caddyfile 18 | end script -------------------------------------------------------------------------------- /caddy/restart_windows.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import "log" 4 | 5 | // Restart restarts Caddy forcefully using newCaddyfile, 6 | // or, if nil, the current/existing Caddyfile is reused. 7 | func Restart(newCaddyfile Input) error { 8 | log.Println("[INFO] Restarting") 9 | 10 | if newCaddyfile == nil { 11 | caddyfileMu.Lock() 12 | newCaddyfile = caddyfile 13 | caddyfileMu.Unlock() 14 | } 15 | 16 | return restartInProc(newCaddyfile) 17 | } 18 | -------------------------------------------------------------------------------- /caddy/setup/proxy.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/mholt/caddy/middleware" 5 | "github.com/mholt/caddy/middleware/proxy" 6 | ) 7 | 8 | // Proxy configures a new Proxy middleware instance. 9 | func Proxy(c *Controller) (middleware.Middleware, error) { 10 | upstreams, err := proxy.NewStaticUpstreams(c.Dispenser) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return func(next middleware.Handler) middleware.Handler { 15 | return proxy.Proxy{Next: next, Upstreams: upstreams} 16 | }, nil 17 | } 18 | -------------------------------------------------------------------------------- /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/bin/caddy` and execute `sudo setcap cap_net_bind_service=+ep /usr/bin/caddy`. 11 | * Save the upstart config file in `/etc/init/caddy.conf`. 12 | * Create a Caddyfile in `/etc/caddy/Caddyfile`. 13 | * Now you can use `sudo service caddy start|stop|restart`. -------------------------------------------------------------------------------- /middleware/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 | -------------------------------------------------------------------------------- /caddy/parse/parse_test.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestAllTokens(t *testing.T) { 9 | input := strings.NewReader("a b c\nd e") 10 | expected := []string{"a", "b", "c", "d", "e"} 11 | tokens := allTokens(input) 12 | 13 | if len(tokens) != len(expected) { 14 | t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens)) 15 | } 16 | 17 | for i, val := range expected { 18 | if tokens[i].text != val { 19 | t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /middleware/roller.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "io" 5 | 6 | "gopkg.in/natefinch/lumberjack.v2" 7 | ) 8 | 9 | // LogRoller implements a middleware that provides a rolling logger. 10 | type LogRoller struct { 11 | Filename string 12 | MaxSize int 13 | MaxAge int 14 | MaxBackups int 15 | LocalTime bool 16 | } 17 | 18 | // GetLogWriter returns an io.Writer that writes to a rolling logger. 19 | func (l LogRoller) GetLogWriter() io.Writer { 20 | return &lumberjack.Logger{ 21 | Filename: l.Filename, 22 | MaxSize: l.MaxSize, 23 | MaxAge: l.MaxAge, 24 | MaxBackups: l.MaxBackups, 25 | LocalTime: l.LocalTime, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /caddy/setup/pprof_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import "testing" 4 | 5 | func TestPProf(t *testing.T) { 6 | tests := []struct { 7 | input string 8 | shouldErr bool 9 | }{ 10 | {`pprof`, false}, 11 | {`pprof {}`, true}, 12 | {`pprof /foo`, true}, 13 | {`pprof { 14 | a b 15 | }`, true}, 16 | {`pprof 17 | pprof`, true}, 18 | } 19 | for i, test := range tests { 20 | c := NewTestController(test.input) 21 | _, err := PProf(c) 22 | if test.shouldErr && err == nil { 23 | t.Errorf("Test %v: Expected error but found nil", i) 24 | } else if !test.shouldErr && err != nil { 25 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /caddy/setup/pprof.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/mholt/caddy/middleware" 5 | "github.com/mholt/caddy/middleware/pprof" 6 | ) 7 | 8 | //PProf returns a new instance of a pprof handler. It accepts no arguments or options. 9 | func PProf(c *Controller) (middleware.Middleware, error) { 10 | found := false 11 | for c.Next() { 12 | if found { 13 | return nil, c.Err("pprof can only be specified once") 14 | } 15 | if len(c.RemainingArgs()) != 0 { 16 | return nil, c.ArgErr() 17 | } 18 | if c.NextBlock() { 19 | return nil, c.ArgErr() 20 | } 21 | found = true 22 | } 23 | 24 | return func(next middleware.Handler) middleware.Handler { 25 | return &pprof.Handler{Next: next, Mux: pprof.NewMux()} 26 | }, nil 27 | } 28 | -------------------------------------------------------------------------------- /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 | CGO_ENABLED: 0 10 | 11 | install: 12 | - rmdir c:\go /s /q 13 | - appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.windows-amd64.zip 14 | - 7z x go1.6.windows-amd64.zip -y -oC:\ > NUL 15 | - go version 16 | - go env 17 | - go get -t ./... 18 | - go get github.com/golang/lint/golint 19 | - go get github.com/gordonklaus/ineffassign 20 | - set PATH=%GOPATH%\bin;%PATH% 21 | 22 | build: off 23 | 24 | test_script: 25 | - go vet ./... 26 | - go test ./... 27 | - ineffassign . 28 | 29 | after_test: 30 | - golint ./... 31 | 32 | deploy: off 33 | -------------------------------------------------------------------------------- /caddy/assets/path.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | ) 8 | 9 | // Path returns the path to the folder 10 | // where the application may store data. This 11 | // currently resolves to ~/.caddy 12 | func Path() string { 13 | return filepath.Join(userHomeDir(), ".caddy") 14 | } 15 | 16 | // userHomeDir returns the user's home directory according to 17 | // environment variables. 18 | // 19 | // Credit: http://stackoverflow.com/a/7922977/1048862 20 | func userHomeDir() string { 21 | if runtime.GOOS == "windows" { 22 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 23 | if home == "" { 24 | home = os.Getenv("USERPROFILE") 25 | } 26 | return home 27 | } 28 | return os.Getenv("HOME") 29 | } 30 | -------------------------------------------------------------------------------- /caddy/setup/internal.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/mholt/caddy/middleware" 5 | "github.com/mholt/caddy/middleware/inner" 6 | ) 7 | 8 | // Internal configures a new Internal middleware instance. 9 | func Internal(c *Controller) (middleware.Middleware, error) { 10 | paths, err := internalParse(c) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return func(next middleware.Handler) middleware.Handler { 16 | return inner.Internal{Next: next, Paths: paths} 17 | }, nil 18 | } 19 | 20 | func internalParse(c *Controller) ([]string, error) { 21 | var paths []string 22 | 23 | for c.Next() { 24 | if !c.NextArg() { 25 | return paths, c.ArgErr() 26 | } 27 | paths = append(paths, c.Val()) 28 | } 29 | 30 | return paths, nil 31 | } 32 | -------------------------------------------------------------------------------- /dist/init/linux-systemd/caddy@.service: -------------------------------------------------------------------------------- 1 | ; see `man systemd.unit` for configuration details 2 | ; the man section also explains *specifiers* `%x` 3 | 4 | [Unit] 5 | Description=Caddy HTTP/2 web server %I 6 | Documentation=https://caddyserver.com/docs 7 | After=network-online.target 8 | Wants=network-online.target 9 | Wants=systemd-networkd-wait-online.service 10 | 11 | [Service] 12 | ; run user and group for caddy 13 | User=%i 14 | Group=http 15 | ExecStart=/usr/bin/caddy -agree=true -conf=/etc/caddy/Caddyfile 16 | Restart=on-failure 17 | ; create a private temp folder that is not shared with other processes 18 | PrivateTmp=true 19 | ; limit the number of file descriptors, see `man systemd.exec` for more limit settings 20 | LimitNOFILE=8192 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /caddy/setup/root.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/mholt/caddy/middleware" 8 | ) 9 | 10 | // Root sets up the root file path of the server. 11 | func Root(c *Controller) (middleware.Middleware, error) { 12 | for c.Next() { 13 | if !c.NextArg() { 14 | return nil, c.ArgErr() 15 | } 16 | c.Root = c.Val() 17 | } 18 | 19 | // Check if root path exists 20 | _, err := os.Stat(c.Root) 21 | if err != nil { 22 | if os.IsNotExist(err) { 23 | // Allow this, because the folder might appear later. 24 | // But make sure the user knows! 25 | log.Printf("[WARNING] Root path does not exist: %s", c.Root) 26 | } else { 27 | return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err) 28 | } 29 | } 30 | 31 | return nil, nil 32 | } 33 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | *If you are filing a bug report, please answer these questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. 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. What did you expect to see? 18 | 19 | 20 | #### 6. What did you see instead (give full error messages and/or log)? 21 | 22 | 23 | #### 7. How can someone who is starting from scratch reproduce this behavior as minimally as possible? 24 | 25 | -------------------------------------------------------------------------------- /caddy/caddy_test.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestCaddyStartStop(t *testing.T) { 10 | caddyfile := "localhost:1984" 11 | 12 | for i := 0; i < 2; i++ { 13 | err := Start(CaddyfileInput{Contents: []byte(caddyfile)}) 14 | if err != nil { 15 | t.Fatalf("Error starting, iteration %d: %v", i, err) 16 | } 17 | 18 | client := http.Client{ 19 | Timeout: time.Duration(2 * time.Second), 20 | } 21 | resp, err := client.Get("http://localhost:1984") 22 | if err != nil { 23 | t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err) 24 | } 25 | resp.Body.Close() 26 | 27 | err = Stop() 28 | if err != nil { 29 | t.Fatalf("Error stopping, iteration %d: %v", i, err) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /caddy/restartinproc.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import "log" 4 | 5 | // restartInProc restarts Caddy forcefully in process using newCaddyfile. 6 | func restartInProc(newCaddyfile Input) error { 7 | wg.Add(1) // barrier so Wait() doesn't unblock 8 | 9 | err := Stop() 10 | if err != nil { 11 | return err 12 | } 13 | 14 | caddyfileMu.Lock() 15 | oldCaddyfile := caddyfile 16 | caddyfileMu.Unlock() 17 | 18 | err = Start(newCaddyfile) 19 | if err != nil { 20 | // revert to old Caddyfile 21 | if oldErr := Start(oldCaddyfile); oldErr != nil { 22 | log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr) 23 | } else { 24 | wg.Done() // take down our barrier 25 | } 26 | return err 27 | } 28 | 29 | wg.Done() // take down our barrier 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /caddy/directives_test.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestRegister(t *testing.T) { 9 | directives := []directive{ 10 | {"dummy", nil}, 11 | {"dummy2", nil}, 12 | } 13 | directiveOrder = directives 14 | RegisterDirective("foo", nil, "dummy") 15 | if len(directiveOrder) != 3 { 16 | t.Fatal("Should have 3 directives now") 17 | } 18 | getNames := func() (s []string) { 19 | for _, d := range directiveOrder { 20 | s = append(s, d.name) 21 | } 22 | return s 23 | } 24 | if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) { 25 | t.Fatalf("directive order doesn't match: %s", getNames()) 26 | } 27 | RegisterDirective("bar", nil, "ASDASD") 28 | if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) { 29 | t.Fatalf("directive order doesn't match: %s", getNames()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dist/README.txt: -------------------------------------------------------------------------------- 1 | CADDY 0.8.3 2 | 3 | Website 4 | https://caddyserver.com 5 | 6 | Twitter 7 | @caddyserver 8 | 9 | Source Code 10 | https://github.com/mholt/caddy 11 | https://github.com/caddyserver 12 | 13 | 14 | For instructions on using Caddy, please see the user guide on the website. 15 | For a list of what's new in this version, see CHANGES.txt. 16 | 17 | Please consider donating to the project if you think it is helpful, 18 | especially if your company is using Caddy. There are also sponsorship 19 | opportunities available! 20 | 21 | If you have a question, bug report, or would like to contribute, please open an 22 | issue or submit a pull request on GitHub. Your contributions do not go unnoticed! 23 | 24 | For a good time, follow @mholt6 on Twitter. 25 | 26 | And thanks - you're awesome! 27 | 28 | 29 | --- 30 | (c) 2015 - 2016 Matthew Holt 31 | -------------------------------------------------------------------------------- /server/config_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "testing" 4 | 5 | func TestConfigAddress(t *testing.T) { 6 | cfg := Config{Host: "foobar", Port: "1234"} 7 | if actual, expected := cfg.Address(), "foobar:1234"; expected != actual { 8 | t.Errorf("Expected '%s' but got '%s'", expected, actual) 9 | } 10 | 11 | cfg = Config{Host: "", Port: "1234"} 12 | if actual, expected := cfg.Address(), ":1234"; expected != actual { 13 | t.Errorf("Expected '%s' but got '%s'", expected, actual) 14 | } 15 | 16 | cfg = Config{Host: "foobar", Port: ""} 17 | if actual, expected := cfg.Address(), "foobar:"; expected != actual { 18 | t.Errorf("Expected '%s' but got '%s'", expected, actual) 19 | } 20 | 21 | cfg = Config{Host: "::1", Port: "443"} 22 | if actual, expected := cfg.Address(), "[::1]:443"; expected != actual { 23 | t.Errorf("Expected '%s' but got '%s'", expected, actual) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /middleware/mime/mime.go: -------------------------------------------------------------------------------- 1 | package mime 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/mholt/caddy/middleware" 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 middleware.Handler 18 | Configs Config 19 | } 20 | 21 | // ServeHTTP implements the middleware.Handler interface. 22 | func (e Mime) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 23 | 24 | // Get a clean /-path, grab the extension 25 | ext := path.Ext(path.Clean(r.URL.Path)) 26 | 27 | if contentType, ok := e.Configs[ext]; ok { 28 | w.Header().Set("Content-Type", contentType) 29 | } 30 | 31 | return e.Next.ServeHTTP(w, r) 32 | } 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | - tip 6 | 7 | env: 8 | - CGO_ENABLED=0 9 | 10 | before_install: 11 | # Decrypts a script that installs an authenticated cookie 12 | # for git to use when cloning from googlesource.com. 13 | # Bypasses "bandwidth limit exceeded" errors. 14 | # See github.com/golang/go/issues/12933 15 | - 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 16 | 17 | install: 18 | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi 19 | - go get -t ./... 20 | - go get github.com/golang/lint/golint 21 | - go get github.com/gordonklaus/ineffassign 22 | 23 | script: 24 | - diff <(echo -n) <(gofmt -s -d .) 25 | - ineffassign . 26 | - go vet ./... 27 | - go test ./... 28 | 29 | after_script: 30 | - golint ./... 31 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /caddy/setup/roller.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/mholt/caddy/middleware" 7 | ) 8 | 9 | func parseRoller(c *Controller) (*middleware.LogRoller, error) { 10 | var size, age, keep int 11 | // This is kind of a hack to support nested blocks: 12 | // As we are already in a block: either log or errors, 13 | // c.nesting > 0 but, as soon as c meets a }, it thinks 14 | // the block is over and return false for c.NextBlock. 15 | for c.NextBlock() { 16 | what := c.Val() 17 | if !c.NextArg() { 18 | return nil, c.ArgErr() 19 | } 20 | value := c.Val() 21 | var err error 22 | switch what { 23 | case "size": 24 | size, err = strconv.Atoi(value) 25 | case "age": 26 | age, err = strconv.Atoi(value) 27 | case "keep": 28 | keep, err = strconv.Atoi(value) 29 | } 30 | if err != nil { 31 | return nil, err 32 | } 33 | } 34 | return &middleware.LogRoller{ 35 | MaxSize: size, 36 | MaxAge: age, 37 | MaxBackups: keep, 38 | LocalTime: true, 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /caddy/setup/expvar_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/expvar" 7 | ) 8 | 9 | func TestExpvar(t *testing.T) { 10 | c := NewTestController(`expvar`) 11 | mid, err := ExpVar(c) 12 | if err != nil { 13 | t.Errorf("Expected no errors, got: %v", err) 14 | } 15 | if mid == nil { 16 | t.Fatal("Expected middleware, was nil instead") 17 | } 18 | 19 | c = NewTestController(`expvar /d/v`) 20 | mid, err = ExpVar(c) 21 | if err != nil { 22 | t.Errorf("Expected no errors, got: %v", err) 23 | } 24 | if mid == nil { 25 | t.Fatal("Expected middleware, was nil instead") 26 | } 27 | 28 | handler := mid(EmptyNext) 29 | myHandler, ok := handler.(expvar.ExpVar) 30 | if !ok { 31 | t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler) 32 | } 33 | if myHandler.Resource != "/d/v" { 34 | t.Errorf("Expected /d/v as expvar resource") 35 | } 36 | if !SameNext(myHandler.Next, EmptyNext) { 37 | t.Error("'Next' field of handler was not set properly") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 uintended 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. -------------------------------------------------------------------------------- /middleware/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/middleware" 10 | ) 11 | 12 | func TestExpVar(t *testing.T) { 13 | rw := ExpVar{ 14 | Next: middleware.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 | -------------------------------------------------------------------------------- /middleware/markdown/watcher.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // DefaultInterval is the default interval at which the markdown watcher 9 | // checks for changes. 10 | const DefaultInterval = time.Second * 60 11 | 12 | // Watch monitors the configured markdown directory for changes. It calls GenerateLinks 13 | // when there are changes. 14 | func Watch(md Markdown, c *Config, interval time.Duration) (stopChan chan struct{}) { 15 | return TickerFunc(interval, func() { 16 | if err := GenerateStatic(md, c); err != nil { 17 | log.Printf("[ERROR] markdown: Re-generating static site: %v", err) 18 | } 19 | }) 20 | } 21 | 22 | // TickerFunc runs f at interval. A message to the returned channel will stop the 23 | // executing goroutine. 24 | func TickerFunc(interval time.Duration, f func()) chan struct{} { 25 | stopChan := make(chan struct{}) 26 | 27 | ticker := time.NewTicker(interval) 28 | go func() { 29 | loop: 30 | for { 31 | select { 32 | case <-ticker.C: 33 | f() 34 | case <-stopChan: 35 | ticker.Stop() 36 | break loop 37 | } 38 | } 39 | }() 40 | 41 | return stopChan 42 | } 43 | -------------------------------------------------------------------------------- /server/virtualhost.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mholt/caddy/middleware" 7 | ) 8 | 9 | // virtualHost represents a virtual host/server. While a Server 10 | // is what actually binds to the address, a user may want to serve 11 | // multiple sites on a single address, and this is what a 12 | // virtualHost allows us to do. 13 | type virtualHost struct { 14 | config Config 15 | fileServer middleware.Handler 16 | stack middleware.Handler 17 | } 18 | 19 | // buildStack builds the server's middleware stack based 20 | // on its config. This method should be called last before 21 | // ListenAndServe begins. 22 | func (vh *virtualHost) buildStack() error { 23 | vh.fileServer = middleware.FileServer(http.Dir(vh.config.Root), []string{vh.config.ConfigFile}) 24 | vh.compile(vh.config.Middleware) 25 | return nil 26 | } 27 | 28 | // compile is an elegant alternative to nesting middleware function 29 | // calls like handler1(handler2(handler3(finalHandler))). 30 | func (vh *virtualHost) compile(layers []middleware.Middleware) { 31 | vh.stack = vh.fileServer // core app layer 32 | for i := len(layers) - 1; i >= 0; i-- { 33 | vh.stack = layers[i](vh.stack) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /middleware/markdown/watcher_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestWatcher(t *testing.T) { 12 | expected := "12345678" 13 | interval := time.Millisecond * 100 14 | i := 0 15 | out := "" 16 | syncChan := make(chan struct{}) 17 | stopChan := TickerFunc(interval, func() { 18 | i++ 19 | out += fmt.Sprint(i) 20 | syncChan <- struct{}{} 21 | }) 22 | sleepInSync(8, syncChan, stopChan) 23 | if out != expected { 24 | t.Fatalf("Expected to have prefix %v, found %v", expected, out) 25 | } 26 | out = "" 27 | i = 0 28 | var mu sync.Mutex 29 | stopChan = TickerFunc(interval, func() { 30 | i++ 31 | mu.Lock() 32 | out += fmt.Sprint(i) 33 | mu.Unlock() 34 | syncChan <- struct{}{} 35 | }) 36 | sleepInSync(9, syncChan, stopChan) 37 | mu.Lock() 38 | res := out 39 | mu.Unlock() 40 | if !strings.HasPrefix(res, expected) || res == expected { 41 | t.Fatalf("expected (%v) must be a proper prefix of out(%v).", expected, out) 42 | } 43 | } 44 | 45 | func sleepInSync(times int, syncChan chan struct{}, stopChan chan struct{}) { 46 | for i := 0; i < times; i++ { 47 | <-syncChan 48 | } 49 | stopChan <- struct{}{} 50 | } 51 | -------------------------------------------------------------------------------- /caddy/parse/parse.go: -------------------------------------------------------------------------------- 1 | // Package parse provides facilities for parsing configuration files. 2 | package parse 3 | 4 | import "io" 5 | 6 | // ServerBlocks parses the input just enough to organize tokens, 7 | // in order, by server block. No further parsing is performed. 8 | // If checkDirectives is true, only valid directives will be allowed 9 | // otherwise we consider it a parse error. Server blocks are returned 10 | // in the order in which they appear. 11 | func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) { 12 | p := parser{Dispenser: NewDispenser(filename, input)} 13 | p.checkDirectives = checkDirectives 14 | blocks, err := p.parseAll() 15 | return blocks, err 16 | } 17 | 18 | // allTokens lexes the entire input, but does not parse it. 19 | // It returns all the tokens from the input, unstructured 20 | // and in order. 21 | func allTokens(input io.Reader) (tokens []token) { 22 | l := new(lexer) 23 | l.load(input) 24 | for l.next() { 25 | tokens = append(tokens, l.token) 26 | } 27 | return 28 | } 29 | 30 | // ValidDirectives is a set of directives that are valid (unordered). Populated 31 | // by config package's init function. 32 | var ValidDirectives = make(map[string]struct{}) 33 | -------------------------------------------------------------------------------- /caddy/https/handler.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "crypto/tls" 5 | "log" 6 | "net/http" 7 | "net/http/httputil" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | const challengeBasePath = "/.well-known/acme-challenge" 13 | 14 | // RequestCallback proxies challenge requests to ACME client if the 15 | // request path starts with challengeBasePath. It returns true if it 16 | // handled the request and no more needs to be done; it returns false 17 | // if this call was a no-op and the request still needs handling. 18 | func RequestCallback(w http.ResponseWriter, r *http.Request) bool { 19 | if strings.HasPrefix(r.URL.Path, challengeBasePath) { 20 | scheme := "http" 21 | if r.TLS != nil { 22 | scheme = "https" 23 | } 24 | 25 | upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort) 26 | if err != nil { 27 | w.WriteHeader(http.StatusInternalServerError) 28 | log.Printf("[ERROR] ACME proxy handler: %v", err) 29 | return true 30 | } 31 | 32 | proxy := httputil.NewSingleHostReverseProxy(upstream) 33 | proxy.Transport = &http.Transport{ 34 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs 35 | } 36 | proxy.ServeHTTP(w, r) 37 | 38 | return true 39 | } 40 | 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /middleware/path.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | const caseSensitivePathEnv = "CASE_SENSITIVE_PATH" 9 | 10 | func init() { 11 | initCaseSettings() 12 | } 13 | 14 | // CaseSensitivePath determines if paths should be case sensitive. 15 | // This is configurable via CASE_SENSITIVE_PATH environment variable. 16 | // It defaults to false. 17 | var CaseSensitivePath = true 18 | 19 | // initCaseSettings loads case sensitivity config from environment variable. 20 | // 21 | // This could have been in init, but init cannot be called from tests. 22 | func initCaseSettings() { 23 | switch os.Getenv(caseSensitivePathEnv) { 24 | case "0", "false": 25 | CaseSensitivePath = false 26 | default: 27 | CaseSensitivePath = true 28 | } 29 | } 30 | 31 | // Path represents a URI path, maybe with pattern characters. 32 | type Path string 33 | 34 | // Matches checks to see if other matches p. 35 | // 36 | // Path matching will probably not always be a direct 37 | // comparison; this method assures that paths can be 38 | // easily and consistently matched. 39 | func (p Path) Matches(other string) bool { 40 | if CaseSensitivePath { 41 | return strings.HasPrefix(string(p), other) 42 | } 43 | return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other)) 44 | } 45 | -------------------------------------------------------------------------------- /middleware/expvar/expvar.go: -------------------------------------------------------------------------------- 1 | package expvar 2 | 3 | import ( 4 | "expvar" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/mholt/caddy/middleware" 9 | ) 10 | 11 | // ExpVar is a simple struct to hold expvar's configuration 12 | type ExpVar struct { 13 | Next middleware.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 middleware.Path(r.URL.Path).Matches(string(e.Resource)) { 21 | expvarHandler(w, r) 22 | return 0, nil 23 | } 24 | 25 | return e.Next.ServeHTTP(w, r) 26 | } 27 | 28 | // expvarHandler returns a JSON object will all the published variables. 29 | // 30 | // This is lifted straight from the expvar package. 31 | func expvarHandler(w http.ResponseWriter, r *http.Request) { 32 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 33 | fmt.Fprintf(w, "{\n") 34 | first := true 35 | expvar.Do(func(kv expvar.KeyValue) { 36 | if !first { 37 | fmt.Fprintf(w, ",\n") 38 | } 39 | first = false 40 | fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value) 41 | }) 42 | fmt.Fprintf(w, "\n}\n") 43 | } 44 | 45 | // Resource contains the path to the expvar entry point 46 | type Resource string 47 | -------------------------------------------------------------------------------- /middleware/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 | } 26 | 27 | uri := func(r *url.URL) string { 28 | uri := r.Path 29 | if r.RawQuery != "" { 30 | uri += "?" + r.RawQuery 31 | } 32 | return uri 33 | } 34 | for i, test := range tests { 35 | r, err := http.NewRequest("GET", test.url, nil) 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | To(fs, r, test.to, newReplacer(r)) 40 | if uri(r.URL) != test.expected { 41 | t.Errorf("Test %v: expected %v found %v", i, test.expected, uri(r.URL)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /middleware/pprof/pprof.go: -------------------------------------------------------------------------------- 1 | package pprof 2 | 3 | import ( 4 | "net/http" 5 | pp "net/http/pprof" 6 | 7 | "github.com/mholt/caddy/middleware" 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 middleware.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 middleware.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+"/", pp.Index) 36 | mux.HandleFunc(BasePath+"/cmdline", pp.Cmdline) 37 | mux.HandleFunc(BasePath+"/profile", pp.Profile) 38 | mux.HandleFunc(BasePath+"/symbol", pp.Symbol) 39 | mux.HandleFunc(BasePath+"/trace", pp.Trace) 40 | return mux 41 | } 42 | -------------------------------------------------------------------------------- /middleware/recorder_test.go: -------------------------------------------------------------------------------- 1 | package middleware 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 | -------------------------------------------------------------------------------- /middleware/headers/headers_test.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | 9 | "github.com/mholt/caddy/middleware" 10 | ) 11 | 12 | func TestHeaders(t *testing.T) { 13 | hostname, err := os.Hostname() 14 | if err != nil { 15 | t.Fatalf("Could not determine hostname: %v", err) 16 | } 17 | for i, test := range []struct { 18 | from string 19 | name string 20 | value string 21 | }{ 22 | {"/a", "Foo", "Bar"}, 23 | {"/a", "Bar", ""}, 24 | {"/a", "Baz", ""}, 25 | {"/a", "ServerName", hostname}, 26 | {"/b", "Foo", ""}, 27 | {"/b", "Bar", "Removed in /a"}, 28 | } { 29 | he := Headers{ 30 | Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 31 | return 0, nil 32 | }), 33 | Rules: []Rule{ 34 | {Path: "/a", Headers: []Header{ 35 | {Name: "Foo", Value: "Bar"}, 36 | {Name: "ServerName", Value: "{hostname}"}, 37 | {Name: "-Bar"}, 38 | }}, 39 | }, 40 | } 41 | 42 | req, err := http.NewRequest("GET", test.from, nil) 43 | if err != nil { 44 | t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) 45 | } 46 | 47 | rec := httptest.NewRecorder() 48 | rec.Header().Set("Bar", "Removed in /a") 49 | 50 | he.ServeHTTP(rec, req) 51 | 52 | if got := rec.Header().Get(test.name); got != test.value { 53 | t.Errorf("Test %d: Expected %s header to be %q but was %q", 54 | i, test.name, test.value, got) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /middleware/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/middleware" 10 | ) 11 | 12 | func TestServeHTTP(t *testing.T) { 13 | h := Handler{ 14 | Next: middleware.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 thru, 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 | -------------------------------------------------------------------------------- /caddy/https/crypto.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "errors" 10 | "io/ioutil" 11 | "os" 12 | ) 13 | 14 | // loadPrivateKey loads a PEM-encoded ECC/RSA private key from file. 15 | func loadPrivateKey(file string) (crypto.PrivateKey, error) { 16 | keyBytes, err := ioutil.ReadFile(file) 17 | if err != nil { 18 | return nil, err 19 | } 20 | keyBlock, _ := pem.Decode(keyBytes) 21 | 22 | switch keyBlock.Type { 23 | case "RSA PRIVATE KEY": 24 | return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) 25 | case "EC PRIVATE KEY": 26 | return x509.ParseECPrivateKey(keyBlock.Bytes) 27 | } 28 | 29 | return nil, errors.New("unknown private key type") 30 | } 31 | 32 | // savePrivateKey saves a PEM-encoded ECC/RSA private key to file. 33 | func savePrivateKey(key crypto.PrivateKey, file string) error { 34 | var pemType string 35 | var keyBytes []byte 36 | switch key := key.(type) { 37 | case *ecdsa.PrivateKey: 38 | var err error 39 | pemType = "EC" 40 | keyBytes, err = x509.MarshalECPrivateKey(key) 41 | if err != nil { 42 | return err 43 | } 44 | case *rsa.PrivateKey: 45 | pemType = "RSA" 46 | keyBytes = x509.MarshalPKCS1PrivateKey(key) 47 | } 48 | 49 | pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes} 50 | keyOut, err := os.Create(file) 51 | if err != nil { 52 | return err 53 | } 54 | keyOut.Chmod(0600) 55 | defer keyOut.Close() 56 | return pem.Encode(keyOut, &pemKey) 57 | } 58 | -------------------------------------------------------------------------------- /caddy/setup/ext.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/mholt/caddy/middleware" 8 | "github.com/mholt/caddy/middleware/extensions" 9 | ) 10 | 11 | // Ext configures a new instance of 'extensions' middleware for clean URLs. 12 | func Ext(c *Controller) (middleware.Middleware, error) { 13 | root := c.Root 14 | 15 | exts, err := extParse(c) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return func(next middleware.Handler) middleware.Handler { 21 | return extensions.Ext{ 22 | Next: next, 23 | Extensions: exts, 24 | Root: root, 25 | } 26 | }, nil 27 | } 28 | 29 | // extParse sets up an instance of extension middleware 30 | // from a middleware controller and returns a list of extensions. 31 | func extParse(c *Controller) ([]string, error) { 32 | var exts []string 33 | 34 | for c.Next() { 35 | // At least one extension is required 36 | if !c.NextArg() { 37 | return exts, c.ArgErr() 38 | } 39 | exts = append(exts, c.Val()) 40 | 41 | // Tack on any other extensions that may have been listed 42 | exts = append(exts, c.RemainingArgs()...) 43 | } 44 | 45 | return exts, nil 46 | } 47 | 48 | // resourceExists returns true if the file specified at 49 | // root + path exists; false otherwise. 50 | func resourceExists(root, path string) bool { 51 | _, err := os.Stat(filepath.Join(root, path)) 52 | // technically we should use os.IsNotExist(err) 53 | // but we don't handle any other kinds of errors anyway 54 | return err == nil 55 | } 56 | -------------------------------------------------------------------------------- /middleware/extensions/ext_test.go: -------------------------------------------------------------------------------- 1 | package extensions 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/mholt/caddy/middleware" 11 | ) 12 | 13 | func TestExtensions(t *testing.T) { 14 | rootDir := os.TempDir() 15 | 16 | // create a temporary page 17 | path := filepath.Join(rootDir, "extensions_test.html") 18 | _, err := os.Create(path) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer os.Remove(path) 23 | 24 | for i, test := range []struct { 25 | path string 26 | extensions []string 27 | expectedURL string 28 | }{ 29 | {"/extensions_test", []string{".html"}, "/extensions_test.html"}, 30 | {"/extensions_test/", []string{".html"}, "/extensions_test/"}, 31 | {"/extensions_test", []string{".json"}, "/extensions_test"}, 32 | {"/another_test", []string{".html"}, "/another_test"}, 33 | {"", []string{".html"}, ""}, 34 | } { 35 | ex := Ext{ 36 | Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 37 | return 0, nil 38 | }), 39 | Root: rootDir, 40 | Extensions: test.extensions, 41 | } 42 | 43 | req, err := http.NewRequest("GET", test.path, nil) 44 | if err != nil { 45 | t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) 46 | } 47 | 48 | rec := httptest.NewRecorder() 49 | 50 | ex.ServeHTTP(rec, req) 51 | 52 | if got := req.URL.String(); got != test.expectedURL { 53 | t.Fatalf("Test %d: Got unexpected request URL: %q, wanted %q", i, got, test.expectedURL) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /middleware/headers/headers.go: -------------------------------------------------------------------------------- 1 | // Package headers provides middleware that appends headers to 2 | // requests based on a set of configuration rules that define 3 | // which routes receive which headers. 4 | package headers 5 | 6 | import ( 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/mholt/caddy/middleware" 11 | ) 12 | 13 | // Headers is middleware that adds headers to the responses 14 | // for requests matching a certain path. 15 | type Headers struct { 16 | Next middleware.Handler 17 | Rules []Rule 18 | } 19 | 20 | // ServeHTTP implements the middleware.Handler interface and serves requests, 21 | // setting headers on the response according to the configured rules. 22 | func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 23 | replacer := middleware.NewReplacer(r, nil, "") 24 | for _, rule := range h.Rules { 25 | if middleware.Path(r.URL.Path).Matches(rule.Path) { 26 | for _, header := range rule.Headers { 27 | if strings.HasPrefix(header.Name, "-") { 28 | w.Header().Del(strings.TrimLeft(header.Name, "-")) 29 | } else { 30 | w.Header().Set(header.Name, replacer.Replace(header.Value)) 31 | } 32 | } 33 | } 34 | } 35 | return h.Next.ServeHTTP(w, r) 36 | } 37 | 38 | type ( 39 | // Rule groups a slice of HTTP headers by a URL pattern. 40 | // TODO: use http.Header type instead? 41 | Rule struct { 42 | Path string 43 | Headers []Header 44 | } 45 | 46 | // Header represents a single HTTP header, simply a name and value. 47 | Header struct { 48 | Name string 49 | Value string 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /middleware/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/mholt/caddy/middleware" 12 | ) 13 | 14 | type erroringMiddleware struct{} 15 | 16 | func (erroringMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 17 | if rr, ok := w.(*middleware.ResponseRecorder); ok { 18 | rr.Replacer.Set("testval", "foobar") 19 | } 20 | return http.StatusNotFound, nil 21 | } 22 | 23 | func TestLoggedStatus(t *testing.T) { 24 | var f bytes.Buffer 25 | var next erroringMiddleware 26 | rule := Rule{ 27 | PathScope: "/", 28 | Format: DefaultLogFormat + " {testval}", 29 | Log: log.New(&f, "", 0), 30 | } 31 | 32 | logger := Logger{ 33 | Rules: []Rule{rule}, 34 | Next: next, 35 | } 36 | 37 | r, err := http.NewRequest("GET", "/", nil) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | rec := httptest.NewRecorder() 43 | 44 | status, err := logger.ServeHTTP(rec, r) 45 | if status != 0 { 46 | t.Errorf("Expected status to be 0, but was %d", status) 47 | } 48 | 49 | if err != nil { 50 | t.Errorf("Expected error to be nil, instead got: %v", err) 51 | } 52 | 53 | logged := f.String() 54 | if !strings.Contains(logged, "404 13") { 55 | t.Errorf("Expected log entry to contain '404 13', but it didn't: %s", logged) 56 | } 57 | 58 | // check custom placeholder 59 | if !strings.Contains(logged, "foobar") { 60 | t.Errorf("Expected the log entry to contain 'foobar' (custom placeholder), but it didn't: %s", logged) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /caddy/setup/expvar.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | stdexpvar "expvar" 5 | "runtime" 6 | "sync" 7 | 8 | "github.com/mholt/caddy/middleware" 9 | "github.com/mholt/caddy/middleware/expvar" 10 | ) 11 | 12 | // ExpVar configures a new ExpVar middleware instance. 13 | func ExpVar(c *Controller) (middleware.Middleware, error) { 14 | resource, err := expVarParse(c) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | // publish any extra information/metrics we may want to capture 20 | publishExtraVars() 21 | 22 | expvar := expvar.ExpVar{Resource: resource} 23 | 24 | return func(next middleware.Handler) middleware.Handler { 25 | expvar.Next = next 26 | return expvar 27 | }, nil 28 | } 29 | 30 | func expVarParse(c *Controller) (expvar.Resource, error) { 31 | var resource expvar.Resource 32 | var err error 33 | 34 | for c.Next() { 35 | args := c.RemainingArgs() 36 | switch len(args) { 37 | case 0: 38 | resource = expvar.Resource(defaultExpvarPath) 39 | case 1: 40 | resource = expvar.Resource(args[0]) 41 | default: 42 | return resource, c.ArgErr() 43 | } 44 | } 45 | 46 | return resource, err 47 | } 48 | 49 | func publishExtraVars() { 50 | // By using sync.Once instead of an init() function, we don't clutter 51 | // the app's expvar export unnecessarily, or risk colliding with it. 52 | publishOnce.Do(func() { 53 | stdexpvar.Publish("Goroutines", stdexpvar.Func(func() interface{} { 54 | return runtime.NumGoroutine() 55 | })) 56 | }) 57 | } 58 | 59 | var publishOnce sync.Once // publishing variables should only be done once 60 | var defaultExpvarPath = "/debug/vars" 61 | -------------------------------------------------------------------------------- /middleware/path_test.go: -------------------------------------------------------------------------------- 1 | package middleware 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 | -------------------------------------------------------------------------------- /caddy/setup/mime_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/mime" 7 | ) 8 | 9 | func TestMime(t *testing.T) { 10 | 11 | c := NewTestController(`mime .txt text/plain`) 12 | 13 | mid, err := Mime(c) 14 | if err != nil { 15 | t.Errorf("Expected no errors, but got: %v", err) 16 | } 17 | if mid == nil { 18 | t.Fatal("Expected middleware, was nil instead") 19 | } 20 | 21 | handler := mid(EmptyNext) 22 | myHandler, ok := handler.(mime.Mime) 23 | if !ok { 24 | t.Fatalf("Expected handler to be type Mime, got: %#v", handler) 25 | } 26 | 27 | if !SameNext(myHandler.Next, 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 | c := NewTestController(test.input) 57 | m, err := mimeParse(c) 58 | if test.shouldErr && err == nil { 59 | t.Errorf("Test %v: Expected error but found nil %v", i, m) 60 | } else if !test.shouldErr && err != nil { 61 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestStandaloneTLSTicketKeyRotation(t *testing.T) { 10 | tlsGovChan := make(chan struct{}) 11 | defer close(tlsGovChan) 12 | callSync := make(chan bool, 1) 13 | defer close(callSync) 14 | 15 | oldHook := setSessionTicketKeysTestHook 16 | defer func() { 17 | setSessionTicketKeysTestHook = oldHook 18 | }() 19 | var keysInUse [][32]byte 20 | setSessionTicketKeysTestHook = func(keys [][32]byte) [][32]byte { 21 | keysInUse = keys 22 | callSync <- true 23 | return keys 24 | } 25 | 26 | c := new(tls.Config) 27 | timer := time.NewTicker(time.Millisecond * 1) 28 | 29 | go standaloneTLSTicketKeyRotation(c, timer, tlsGovChan) 30 | 31 | rounds := 0 32 | var lastTicketKey [32]byte 33 | for { 34 | select { 35 | case <-callSync: 36 | if lastTicketKey == keysInUse[0] { 37 | close(tlsGovChan) 38 | t.Errorf("The same TLS ticket key has been used again (not rotated): %x.", lastTicketKey) 39 | return 40 | } 41 | lastTicketKey = keysInUse[0] 42 | rounds++ 43 | if rounds <= tlsNumTickets && len(keysInUse) != rounds { 44 | close(tlsGovChan) 45 | t.Errorf("Expected TLS ticket keys in use: %d; Got instead: %d.", rounds, len(keysInUse)) 46 | return 47 | } 48 | if c.SessionTicketsDisabled == true { 49 | t.Error("Session tickets have been disabled unexpectedly.") 50 | return 51 | } 52 | if rounds >= tlsNumTickets+1 { 53 | return 54 | } 55 | case <-time.After(time.Second * 1): 56 | t.Errorf("Timeout after %d rounds.", rounds) 57 | return 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /middleware/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/middleware" 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 middleware.Handler 23 | 24 | // Path to ther root of the site 25 | Root string 26 | 27 | // List of extensions to try 28 | Extensions []string 29 | } 30 | 31 | // ServeHTTP implements the middleware.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 | -------------------------------------------------------------------------------- /caddy/setup/mime.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mholt/caddy/middleware" 8 | "github.com/mholt/caddy/middleware/mime" 9 | ) 10 | 11 | // Mime configures a new mime middleware instance. 12 | func Mime(c *Controller) (middleware.Middleware, error) { 13 | configs, err := mimeParse(c) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return func(next middleware.Handler) middleware.Handler { 19 | return mime.Mime{Next: next, Configs: configs} 20 | }, nil 21 | } 22 | 23 | func mimeParse(c *Controller) (mime.Config, error) { 24 | configs := mime.Config{} 25 | 26 | for c.Next() { 27 | // At least one extension is required 28 | 29 | args := c.RemainingArgs() 30 | switch len(args) { 31 | case 2: 32 | if err := validateExt(configs, args[0]); err != nil { 33 | return configs, err 34 | } 35 | configs[args[0]] = args[1] 36 | case 1: 37 | return configs, c.ArgErr() 38 | case 0: 39 | for c.NextBlock() { 40 | ext := c.Val() 41 | if err := validateExt(configs, ext); err != nil { 42 | return configs, err 43 | } 44 | if !c.NextArg() { 45 | return configs, c.ArgErr() 46 | } 47 | configs[ext] = c.Val() 48 | } 49 | } 50 | 51 | } 52 | 53 | return configs, nil 54 | } 55 | 56 | // validateExt checks for valid file name extension. 57 | func validateExt(configs mime.Config, ext string) error { 58 | if !strings.HasPrefix(ext, ".") { 59 | return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext) 60 | } 61 | if _, ok := configs[ext]; ok { 62 | return fmt.Errorf(`mime: duplicate extension "%v" found`, ext) 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /middleware/inner/internal_test.go: -------------------------------------------------------------------------------- 1 | package inner 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/mholt/caddy/middleware" 10 | ) 11 | 12 | func TestInternal(t *testing.T) { 13 | im := Internal{ 14 | Next: middleware.HandlerFunc(internalTestHandlerFunc), 15 | Paths: []string{"/internal"}, 16 | } 17 | 18 | tests := []struct { 19 | url string 20 | expectedCode int 21 | expectedBody string 22 | }{ 23 | {"/internal", http.StatusNotFound, ""}, 24 | 25 | {"/public", 0, "/public"}, 26 | {"/public/internal", 0, "/public/internal"}, 27 | 28 | {"/redirect", 0, "/internal"}, 29 | 30 | {"/cycle", http.StatusInternalServerError, ""}, 31 | } 32 | 33 | for i, test := range tests { 34 | req, err := http.NewRequest("GET", test.url, nil) 35 | if err != nil { 36 | t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) 37 | } 38 | 39 | rec := httptest.NewRecorder() 40 | code, _ := im.ServeHTTP(rec, req) 41 | 42 | if code != test.expectedCode { 43 | t.Errorf("Test %d: Expected status code %d for %s, but got %d", 44 | i, test.expectedCode, test.url, code) 45 | } 46 | if rec.Body.String() != test.expectedBody { 47 | t.Errorf("Test %d: Expected body '%s' for %s, but got '%s'", 48 | i, test.expectedBody, test.url, rec.Body.String()) 49 | } 50 | } 51 | } 52 | 53 | func internalTestHandlerFunc(w http.ResponseWriter, r *http.Request) (int, error) { 54 | switch r.URL.Path { 55 | case "/redirect": 56 | w.Header().Set("X-Accel-Redirect", "/internal") 57 | case "/cycle": 58 | w.Header().Set("X-Accel-Redirect", "/cycle") 59 | } 60 | w.WriteHeader(http.StatusOK) 61 | fmt.Fprintf(w, r.URL.String()) 62 | 63 | return 0, nil 64 | } 65 | -------------------------------------------------------------------------------- /middleware/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/middleware" 10 | ) 11 | 12 | func TestMimeHandler(t *testing.T) { 13 | 14 | mimes := Config{ 15 | ".html": "text/html", 16 | ".txt": "text/plain", 17 | ".swf": "application/x-shockwave-flash", 18 | } 19 | 20 | m := Mime{Configs: mimes} 21 | 22 | w := httptest.NewRecorder() 23 | exts := []string{ 24 | ".html", ".txt", ".swf", 25 | } 26 | for _, e := range exts { 27 | url := "/file" + e 28 | r, err := http.NewRequest("GET", url, nil) 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | m.Next = nextFunc(true, mimes[e]) 33 | _, err = m.ServeHTTP(w, r) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | } 38 | 39 | w = httptest.NewRecorder() 40 | exts = []string{ 41 | ".htm1", ".abc", ".mdx", 42 | } 43 | for _, e := range exts { 44 | url := "/file" + e 45 | r, err := http.NewRequest("GET", url, nil) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | m.Next = nextFunc(false, "") 50 | _, err = m.ServeHTTP(w, r) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | } 55 | } 56 | 57 | func nextFunc(shouldMime bool, contentType string) middleware.Handler { 58 | return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 59 | if shouldMime { 60 | if w.Header().Get("Content-Type") != contentType { 61 | return 0, fmt.Errorf("expected Content-Type: %v, found %v", contentType, r.Header.Get("Content-Type")) 62 | } 63 | return 0, nil 64 | } 65 | if w.Header().Get("Content-Type") != "" { 66 | return 0, fmt.Errorf("Content-Type header not expected") 67 | } 68 | return 0, nil 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /caddy/setup/startupshutdown.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/mholt/caddy/middleware" 9 | ) 10 | 11 | // Startup registers a startup callback to execute during server start. 12 | func Startup(c *Controller) (middleware.Middleware, error) { 13 | return nil, registerCallback(c, &c.FirstStartup) 14 | } 15 | 16 | // Shutdown registers a shutdown callback to execute during process exit. 17 | func Shutdown(c *Controller) (middleware.Middleware, error) { 18 | return nil, registerCallback(c, &c.Shutdown) 19 | } 20 | 21 | // registerCallback registers a callback function to execute by 22 | // using c to parse the line. It appends the callback function 23 | // to the list of callback functions passed in by reference. 24 | func registerCallback(c *Controller, list *[]func() error) error { 25 | var funcs []func() error 26 | 27 | for c.Next() { 28 | args := c.RemainingArgs() 29 | if len(args) == 0 { 30 | return c.ArgErr() 31 | } 32 | 33 | nonblock := false 34 | if len(args) > 1 && args[len(args)-1] == "&" { 35 | // Run command in background; non-blocking 36 | nonblock = true 37 | args = args[:len(args)-1] 38 | } 39 | 40 | command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " ")) 41 | if err != nil { 42 | return c.Err(err.Error()) 43 | } 44 | 45 | fn := func() error { 46 | cmd := exec.Command(command, args...) 47 | cmd.Stdin = os.Stdin 48 | cmd.Stdout = os.Stdout 49 | cmd.Stderr = os.Stderr 50 | 51 | if nonblock { 52 | return cmd.Start() 53 | } 54 | return cmd.Run() 55 | } 56 | 57 | funcs = append(funcs, fn) 58 | } 59 | 60 | return c.OncePerServerBlock(func() error { 61 | *list = append(*list, funcs...) 62 | return nil 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /middleware/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/middleware" 11 | ) 12 | 13 | // Redirect is middleware to respond with HTTP redirects 14 | type Redirect struct { 15 | Next middleware.Handler 16 | Rules []Rule 17 | } 18 | 19 | // ServeHTTP implements the middleware.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) { 23 | to := middleware.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, FromPath, To string 44 | Code int 45 | Meta bool 46 | } 47 | 48 | // Script tag comes first since that will better imitate a redirect in the browser's 49 | // history, but the meta tag is a fallback for most non-JS clients. 50 | const metaRedir = ` 51 | 52 | 53 | 54 | 55 | 56 | Redirecting... 57 | ` 58 | -------------------------------------------------------------------------------- /caddy/setup/headers.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/mholt/caddy/middleware" 5 | "github.com/mholt/caddy/middleware/headers" 6 | ) 7 | 8 | // Headers configures a new Headers middleware instance. 9 | func Headers(c *Controller) (middleware.Middleware, error) { 10 | rules, err := headersParse(c) 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | return func(next middleware.Handler) middleware.Handler { 16 | return headers.Headers{Next: next, Rules: rules} 17 | }, nil 18 | } 19 | 20 | func headersParse(c *Controller) ([]headers.Rule, error) { 21 | var rules []headers.Rule 22 | 23 | for c.NextLine() { 24 | var head headers.Rule 25 | var isNewPattern bool 26 | 27 | if !c.NextArg() { 28 | return rules, c.ArgErr() 29 | } 30 | pattern := c.Val() 31 | 32 | // See if we already have a definition for this Path pattern... 33 | for _, h := range rules { 34 | if h.Path == pattern { 35 | head = h 36 | break 37 | } 38 | } 39 | 40 | // ...otherwise, this is a new pattern 41 | if head.Path == "" { 42 | head.Path = pattern 43 | isNewPattern = true 44 | } 45 | 46 | for c.NextBlock() { 47 | // A block of headers was opened... 48 | 49 | h := headers.Header{Name: c.Val()} 50 | 51 | if c.NextArg() { 52 | h.Value = c.Val() 53 | } 54 | 55 | head.Headers = append(head.Headers, h) 56 | } 57 | if c.NextArg() { 58 | // ... or single header was defined as an argument instead. 59 | 60 | h := headers.Header{Name: c.Val()} 61 | 62 | h.Value = c.Val() 63 | 64 | if c.NextArg() { 65 | h.Value = c.Val() 66 | } 67 | 68 | head.Headers = append(head.Headers, h) 69 | } 70 | 71 | if isNewPattern { 72 | rules = append(rules, head) 73 | } else { 74 | for i := 0; i < len(rules); i++ { 75 | if rules[i].Path == pattern { 76 | rules[i] = head 77 | break 78 | } 79 | } 80 | } 81 | } 82 | 83 | return rules, nil 84 | } 85 | -------------------------------------------------------------------------------- /caddy/https/handler_test.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestRequestCallbackNoOp(t *testing.T) { 11 | // try base paths that aren't handled by this handler 12 | for _, url := range []string{ 13 | "http://localhost/", 14 | "http://localhost/foo.html", 15 | "http://localhost/.git", 16 | "http://localhost/.well-known/", 17 | "http://localhost/.well-known/acme-challenging", 18 | } { 19 | req, err := http.NewRequest("GET", url, nil) 20 | if err != nil { 21 | t.Fatalf("Could not craft request, got error: %v", err) 22 | } 23 | rw := httptest.NewRecorder() 24 | if RequestCallback(rw, req) { 25 | t.Errorf("Got true with this URL, but shouldn't have: %s", url) 26 | } 27 | } 28 | } 29 | 30 | func TestRequestCallbackSuccess(t *testing.T) { 31 | expectedPath := challengeBasePath + "/asdf" 32 | 33 | // Set up fake acme handler backend to make sure proxying succeeds 34 | var proxySuccess bool 35 | ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 36 | proxySuccess = true 37 | if r.URL.Path != expectedPath { 38 | t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path) 39 | } 40 | })) 41 | 42 | // Custom listener that uses the port we expect 43 | ln, err := net.Listen("tcp", "127.0.0.1:"+AlternatePort) 44 | if err != nil { 45 | t.Fatalf("Unable to start test server listener: %v", err) 46 | } 47 | ts.Listener = ln 48 | 49 | // Start our engines and run the test 50 | ts.Start() 51 | defer ts.Close() 52 | req, err := http.NewRequest("GET", "http://127.0.0.1:"+AlternatePort+expectedPath, nil) 53 | if err != nil { 54 | t.Fatalf("Could not craft request, got error: %v", err) 55 | } 56 | rw := httptest.NewRecorder() 57 | 58 | RequestCallback(rw, req) 59 | 60 | if !proxySuccess { 61 | t.Fatal("Expected request to be proxied, but it wasn't") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /caddy/setup/startupshutdown_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // The Startup function's tests are symmetrical to Shutdown tests, 12 | // because the Startup and Shutdown functions share virtually the 13 | // same functionality 14 | func TestStartup(t *testing.T) { 15 | tempDirPath, err := getTempDirPath() 16 | if err != nil { 17 | t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) 18 | } 19 | 20 | testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown") 21 | defer func() { 22 | // clean up after non-blocking startup function quits 23 | time.Sleep(500 * time.Millisecond) 24 | os.RemoveAll(testDir) 25 | }() 26 | osSenitiveTestDir := filepath.FromSlash(testDir) 27 | os.RemoveAll(osSenitiveTestDir) // start with a clean slate 28 | 29 | tests := []struct { 30 | input string 31 | shouldExecutionErr bool 32 | shouldRemoveErr bool 33 | }{ 34 | // test case #0 tests proper functionality blocking commands 35 | {"startup mkdir " + osSenitiveTestDir, false, false}, 36 | 37 | // test case #1 tests proper functionality of non-blocking commands 38 | {"startup mkdir " + osSenitiveTestDir + " &", false, true}, 39 | 40 | // test case #2 tests handling of non-existent commands 41 | {"startup " + strconv.Itoa(int(time.Now().UnixNano())), true, true}, 42 | } 43 | 44 | for i, test := range tests { 45 | c := NewTestController(test.input) 46 | _, err = Startup(c) 47 | if err != nil { 48 | t.Errorf("Expected no errors, got: %v", err) 49 | } 50 | err = c.FirstStartup[0]() 51 | if err != nil && !test.shouldExecutionErr { 52 | t.Errorf("Test %d recieved an error of:\n%v", i, err) 53 | } 54 | err = os.Remove(osSenitiveTestDir) 55 | if err != nil && !test.shouldRemoveErr { 56 | t.Errorf("Test %d recieved an error of:\n%v", i, err) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /caddy/setup/basicauth.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/mholt/caddy/middleware" 7 | "github.com/mholt/caddy/middleware/basicauth" 8 | ) 9 | 10 | // BasicAuth configures a new BasicAuth middleware instance. 11 | func BasicAuth(c *Controller) (middleware.Middleware, error) { 12 | root := c.Root 13 | 14 | rules, err := basicAuthParse(c) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | basic := basicauth.BasicAuth{Rules: rules} 20 | 21 | return func(next middleware.Handler) middleware.Handler { 22 | basic.Next = next 23 | basic.SiteRoot = root 24 | return basic 25 | }, nil 26 | } 27 | 28 | func basicAuthParse(c *Controller) ([]basicauth.Rule, error) { 29 | var rules []basicauth.Rule 30 | 31 | var err error 32 | for c.Next() { 33 | var rule basicauth.Rule 34 | 35 | args := c.RemainingArgs() 36 | 37 | switch len(args) { 38 | case 2: 39 | rule.Username = args[0] 40 | if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil { 41 | return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) 42 | } 43 | 44 | for c.NextBlock() { 45 | rule.Resources = append(rule.Resources, c.Val()) 46 | if c.NextArg() { 47 | return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val()) 48 | } 49 | } 50 | case 3: 51 | rule.Resources = append(rule.Resources, args[0]) 52 | rule.Username = args[1] 53 | if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil { 54 | return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err) 55 | } 56 | default: 57 | return rules, c.ArgErr() 58 | } 59 | 60 | rules = append(rules, rule) 61 | } 62 | 63 | return rules, nil 64 | } 65 | 66 | func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) { 67 | if !strings.HasPrefix(passw, "htpasswd=") { 68 | return basicauth.PlainMatcher(passw), nil 69 | } 70 | 71 | return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot) 72 | } 73 | -------------------------------------------------------------------------------- /caddy/sigtrap.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | 9 | "github.com/mholt/caddy/server" 10 | ) 11 | 12 | // TrapSignals create signal handlers for all applicable signals for this 13 | // system. If your Go program uses signals, this is a rather invasive 14 | // function; best to implement them yourself in that case. Signals are not 15 | // required for the caddy package to function properly, but this is a 16 | // convenient way to allow the user to control this package of your program. 17 | func TrapSignals() { 18 | trapSignalsCrossPlatform() 19 | trapSignalsPosix() 20 | } 21 | 22 | // trapSignalsCrossPlatform captures SIGINT, which triggers forceful 23 | // shutdown that executes shutdown callbacks first. A second interrupt 24 | // signal will exit the process immediately. 25 | func trapSignalsCrossPlatform() { 26 | go func() { 27 | shutdown := make(chan os.Signal, 1) 28 | signal.Notify(shutdown, os.Interrupt) 29 | 30 | for i := 0; true; i++ { 31 | <-shutdown 32 | 33 | if i > 0 { 34 | log.Println("[INFO] SIGINT: Force quit") 35 | if PidFile != "" { 36 | os.Remove(PidFile) 37 | } 38 | os.Exit(1) 39 | } 40 | 41 | log.Println("[INFO] SIGINT: Shutting down") 42 | 43 | if PidFile != "" { 44 | os.Remove(PidFile) 45 | } 46 | 47 | go os.Exit(executeShutdownCallbacks("SIGINT")) 48 | } 49 | }() 50 | } 51 | 52 | // executeShutdownCallbacks executes the shutdown callbacks as initiated 53 | // by signame. It logs any errors and returns the recommended exit status. 54 | // This function is idempotent; subsequent invocations always return 0. 55 | func executeShutdownCallbacks(signame string) (exitCode int) { 56 | shutdownCallbacksOnce.Do(func() { 57 | serversMu.Lock() 58 | errs := server.ShutdownCallbacks(servers) 59 | serversMu.Unlock() 60 | 61 | if len(errs) > 0 { 62 | for _, err := range errs { 63 | log.Printf("[ERROR] %s shutdown: %v", signame, err) 64 | } 65 | exitCode = 1 66 | } 67 | }) 68 | return 69 | } 70 | 71 | var shutdownCallbacksOnce sync.Once 72 | -------------------------------------------------------------------------------- /dist/init/linux-systemd/README.md: -------------------------------------------------------------------------------- 1 | # systemd unit for caddy 2 | 3 | Please do not hesitate to ask [me](mailto:klingt.net+caddy@gmail.com) if you've any questions. 4 | 5 | ## Quickstart 6 | 7 | - install the unit configuration file: `cp caddy@.service /etc/systemd/system` 8 | - reload the systemd daemon: `systemctl deamon-reload` 9 | - make sure to [configure](#configuration) the service unit before starting caddy 10 | - start caddy: `systemctl start caddy@someuser` 11 | - enable the service (automatically start on boot): `systemctl enable caddy@someuser` 12 | - the `.caddy` folder will be created inside the users home directory that runs caddy, i.e. `/home/someuser/.caddy` for `systemctl start caddy@someuser` 13 | 14 | ## Configuration 15 | 16 | - do not edit the systemd unit directly, use systemd's builtin tools: 17 | - `systemctl edit caddy@` to make user local modifications to the service unit 18 | - `systemctl edit --full caddy@` to make system-wide modifications 19 | - in most cases it's enough to adapt the `ExecStart` directive: 20 | - `systemctl edit caddy@` 21 | - systemd needs absolute paths, therefore make sure that the path to caddy is correct 22 | - example: 23 | 24 | ```ini 25 | [Service] 26 | ; reset the original setting 27 | ExecStart= 28 | ExecStart=/usr/bin/caddy -conf="/etc/caddy/myCaddy.conf" -agree -email="my@mail.address" 29 | ``` 30 | 31 | - to view your configuration use `systemctl cat caddy@` 32 | - double check the permissions of your web root path to make sure that caddy can access it as its run user and group 33 | 34 | ## Tips 35 | 36 | - use `log stdout` and `errors stderr` in your Caddyfile to make use of `journalctl` 37 | - `journalctl` is systemd's log query tool 38 | - lets say you want all the log entries for caddy since the last boot beginning from the last entry: `journalctl --reverse --boot --unit caddy@someuser` 39 | - maybe you want to follow caddys log output: `journalctl -fu caddy@someuser` 40 | - to send a signal to a service units main PID, e.g. let caddy reload its config: `systemctl kill --signal=USR1 caddy@someuser` 41 | -------------------------------------------------------------------------------- /caddy/sigtrap_posix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package caddy 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | // trapSignalsPosix captures POSIX-only signals. 14 | func trapSignalsPosix() { 15 | go func() { 16 | sigchan := make(chan os.Signal, 1) 17 | signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1) 18 | 19 | for sig := range sigchan { 20 | switch sig { 21 | case syscall.SIGTERM: 22 | log.Println("[INFO] SIGTERM: Terminating process") 23 | if PidFile != "" { 24 | os.Remove(PidFile) 25 | } 26 | os.Exit(0) 27 | 28 | case syscall.SIGQUIT: 29 | log.Println("[INFO] SIGQUIT: Shutting down") 30 | exitCode := executeShutdownCallbacks("SIGQUIT") 31 | err := Stop() 32 | if err != nil { 33 | log.Printf("[ERROR] SIGQUIT stop: %v", err) 34 | exitCode = 1 35 | } 36 | if PidFile != "" { 37 | os.Remove(PidFile) 38 | } 39 | os.Exit(exitCode) 40 | 41 | case syscall.SIGHUP: 42 | log.Println("[INFO] SIGHUP: Hanging up") 43 | err := Stop() 44 | if err != nil { 45 | log.Printf("[ERROR] SIGHUP stop: %v", err) 46 | } 47 | 48 | case syscall.SIGUSR1: 49 | log.Println("[INFO] SIGUSR1: Reloading") 50 | 51 | var updatedCaddyfile Input 52 | 53 | caddyfileMu.Lock() 54 | if caddyfile == nil { 55 | // Hmm, did spawing process forget to close stdin? Anyhow, this is unusual. 56 | log.Println("[ERROR] SIGUSR1: no Caddyfile to reload (was stdin left open?)") 57 | caddyfileMu.Unlock() 58 | continue 59 | } 60 | if caddyfile.IsFile() { 61 | body, err := ioutil.ReadFile(caddyfile.Path()) 62 | if err == nil { 63 | updatedCaddyfile = CaddyfileInput{ 64 | Filepath: caddyfile.Path(), 65 | Contents: body, 66 | RealFile: true, 67 | } 68 | } 69 | } 70 | caddyfileMu.Unlock() 71 | 72 | err := Restart(updatedCaddyfile) 73 | if err != nil { 74 | log.Printf("[ERROR] SIGUSR1: %v", err) 75 | } 76 | } 77 | } 78 | }() 79 | } 80 | -------------------------------------------------------------------------------- /caddy/setup/internal_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/inner" 7 | ) 8 | 9 | func TestInternal(t *testing.T) { 10 | c := NewTestController(`internal /internal`) 11 | 12 | mid, err := Internal(c) 13 | 14 | if err != nil { 15 | t.Errorf("Expected no errors, got: %v", err) 16 | } 17 | 18 | if mid == nil { 19 | t.Fatal("Expected middleware, was nil instead") 20 | } 21 | 22 | handler := mid(EmptyNext) 23 | myHandler, ok := handler.(inner.Internal) 24 | 25 | if !ok { 26 | t.Fatalf("Expected handler to be type Internal, got: %#v", handler) 27 | } 28 | 29 | if myHandler.Paths[0] != "/internal" { 30 | t.Errorf("Expected internal in the list of internal Paths") 31 | } 32 | 33 | if !SameNext(myHandler.Next, EmptyNext) { 34 | t.Error("'Next' field of handler was not set properly") 35 | } 36 | 37 | } 38 | 39 | func TestInternalParse(t *testing.T) { 40 | tests := []struct { 41 | inputInternalPaths string 42 | shouldErr bool 43 | expectedInternalPaths []string 44 | }{ 45 | {`internal /internal`, false, []string{"/internal"}}, 46 | 47 | {`internal /internal1 48 | internal /internal2`, false, []string{"/internal1", "/internal2"}}, 49 | } 50 | for i, test := range tests { 51 | c := NewTestController(test.inputInternalPaths) 52 | actualInternalPaths, err := internalParse(c) 53 | 54 | if err == nil && test.shouldErr { 55 | t.Errorf("Test %d didn't error, but it should have", i) 56 | } else if err != nil && !test.shouldErr { 57 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 58 | } 59 | 60 | if len(actualInternalPaths) != len(test.expectedInternalPaths) { 61 | t.Fatalf("Test %d expected %d InternalPaths, but got %d", 62 | i, len(test.expectedInternalPaths), len(actualInternalPaths)) 63 | } 64 | for j, actualInternalPath := range actualInternalPaths { 65 | if actualInternalPath != test.expectedInternalPaths[j] { 66 | t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s", 67 | i, j, test.expectedInternalPaths[j], actualInternalPath) 68 | } 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /caddy/setup/browse_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/mholt/caddy/middleware/browse" 12 | ) 13 | 14 | func TestBrowse(t *testing.T) { 15 | 16 | tempDirPath, err := getTempDirPath() 17 | if err != nil { 18 | t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) 19 | } 20 | nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano()))) 21 | 22 | tempTemplate, err := ioutil.TempFile(".", "tempTemplate") 23 | if err != nil { 24 | t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err) 25 | } 26 | defer os.Remove(tempTemplate.Name()) 27 | 28 | tempTemplatePath := filepath.Join(".", tempTemplate.Name()) 29 | 30 | for i, test := range []struct { 31 | input string 32 | expectedPathScope []string 33 | shouldErr bool 34 | }{ 35 | // test case #0 tests handling of multiple pathscopes 36 | {"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false}, 37 | 38 | // test case #1 tests instantiation of browse.Config with default values 39 | {"browse /", []string{"/"}, false}, 40 | 41 | // test case #2 tests detectaction of custom template 42 | {"browse . " + tempTemplatePath, []string{"."}, false}, 43 | 44 | // test case #3 tests detection of non-existent template 45 | {"browse . " + nonExistantDirPath, nil, true}, 46 | 47 | // test case #4 tests detection of duplicate pathscopes 48 | {"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true}, 49 | } { 50 | 51 | recievedFunc, err := Browse(NewTestController(test.input)) 52 | if err != nil && !test.shouldErr { 53 | t.Errorf("Test case #%d recieved an error of %v", i, err) 54 | } 55 | if test.expectedPathScope == nil { 56 | continue 57 | } 58 | recievedConfigs := recievedFunc(nil).(browse.Browse).Configs 59 | for j, config := range recievedConfigs { 60 | if config.PathScope != test.expectedPathScope[j] { 61 | t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server/graceful.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "syscall" 7 | ) 8 | 9 | // newGracefulListener returns a gracefulListener that wraps l and 10 | // uses wg (stored in the host server) to count connections. 11 | func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener { 12 | gl := &gracefulListener{ListenerFile: l, stop: make(chan error), httpWg: wg} 13 | go func() { 14 | <-gl.stop 15 | gl.Lock() 16 | gl.stopped = true 17 | gl.Unlock() 18 | gl.stop <- gl.ListenerFile.Close() 19 | }() 20 | return gl 21 | } 22 | 23 | // gracefuListener is a net.Listener which can 24 | // count the number of connections on it. Its 25 | // methods mainly wrap net.Listener to be graceful. 26 | type gracefulListener struct { 27 | ListenerFile 28 | stop chan error 29 | stopped bool 30 | sync.Mutex // protects the stopped flag 31 | httpWg *sync.WaitGroup // pointer to the host's wg used for counting connections 32 | } 33 | 34 | // Accept accepts a connection. 35 | func (gl *gracefulListener) Accept() (c net.Conn, err error) { 36 | c, err = gl.ListenerFile.Accept() 37 | if err != nil { 38 | return 39 | } 40 | c = gracefulConn{Conn: c, httpWg: gl.httpWg} 41 | gl.httpWg.Add(1) 42 | return 43 | } 44 | 45 | // Close immediately closes the listener. 46 | func (gl *gracefulListener) Close() error { 47 | gl.Lock() 48 | if gl.stopped { 49 | gl.Unlock() 50 | return syscall.EINVAL 51 | } 52 | gl.Unlock() 53 | gl.stop <- nil 54 | return <-gl.stop 55 | } 56 | 57 | // gracefulConn represents a connection on a 58 | // gracefulListener so that we can keep track 59 | // of the number of connections, thus facilitating 60 | // a graceful shutdown. 61 | type gracefulConn struct { 62 | net.Conn 63 | httpWg *sync.WaitGroup // pointer to the host server's connection waitgroup 64 | } 65 | 66 | // Close closes c's underlying connection while updating the wg count. 67 | func (c gracefulConn) Close() error { 68 | err := c.Conn.Close() 69 | if err != nil { 70 | return err 71 | } 72 | // close can fail on http2 connections (as of Oct. 2015, before http2 in std lib) 73 | // so don't decrement count unless close succeeds 74 | c.httpWg.Done() 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /caddy/setup/ext_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/extensions" 7 | ) 8 | 9 | func TestExt(t *testing.T) { 10 | c := NewTestController(`ext .html .htm .php`) 11 | 12 | mid, err := Ext(c) 13 | 14 | if err != nil { 15 | t.Errorf("Expected no errors, got: %v", err) 16 | } 17 | 18 | if mid == nil { 19 | t.Fatal("Expected middleware, was nil instead") 20 | } 21 | 22 | handler := mid(EmptyNext) 23 | myHandler, ok := handler.(extensions.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 !SameNext(myHandler.Next, 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 | c := NewTestController(test.inputExts) 56 | actualExts, err := extParse(c) 57 | 58 | if err == nil && test.shouldErr { 59 | t.Errorf("Test %d didn't error, but it should have", i) 60 | } else if err != nil && !test.shouldErr { 61 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 62 | } 63 | 64 | if len(actualExts) != len(test.expectedExts) { 65 | t.Fatalf("Test %d expected %d rules, but got %d", 66 | i, len(test.expectedExts), len(actualExts)) 67 | } 68 | for j, actualExt := range actualExts { 69 | if actualExt != test.expectedExts[j] { 70 | t.Fatalf("Test %d expected %dth extension to be %s , but got %s", 71 | i, j, test.expectedExts[j], actualExt) 72 | } 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /middleware/rewrite/to.go: -------------------------------------------------------------------------------- 1 | package rewrite 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/url" 7 | "path" 8 | "strings" 9 | 10 | "github.com/mholt/caddy/middleware" 11 | ) 12 | 13 | // To attempts rewrite. It attempts to rewrite to first valid path 14 | // or the last path if none of the paths are valid. 15 | // Returns true if rewrite is successful and false otherwise. 16 | func To(fs http.FileSystem, r *http.Request, to string, replacer middleware.Replacer) Result { 17 | tos := strings.Fields(to) 18 | 19 | // try each rewrite paths 20 | t := "" 21 | for _, v := range tos { 22 | t = path.Clean(replacer.Replace(v)) 23 | 24 | // add trailing slash for directories, if present 25 | if strings.HasSuffix(v, "/") && !strings.HasSuffix(t, "/") { 26 | t += "/" 27 | } 28 | 29 | // validate file 30 | if isValidFile(fs, t) { 31 | break 32 | } 33 | } 34 | 35 | // validate resulting path 36 | u, err := url.Parse(t) 37 | if err != nil { 38 | // Let the user know we got here. Rewrite is expected but 39 | // the resulting url is invalid. 40 | log.Printf("[ERROR] rewrite: resulting path '%v' is invalid. error: %v", t, err) 41 | return RewriteIgnored 42 | } 43 | 44 | // take note of this rewrite for internal use by fastcgi 45 | // all we need is the URI, not full URL 46 | r.Header.Set(headerFieldName, r.URL.RequestURI()) 47 | 48 | // perform rewrite 49 | r.URL.Path = u.Path 50 | if u.RawQuery != "" { 51 | // overwrite query string if present 52 | r.URL.RawQuery = u.RawQuery 53 | } 54 | if u.Fragment != "" { 55 | // overwrite fragment if present 56 | r.URL.Fragment = u.Fragment 57 | } 58 | 59 | return RewriteDone 60 | } 61 | 62 | // isValidFile checks if file exists on the filesystem. 63 | // if file ends with `/`, it is validated as a directory. 64 | func isValidFile(fs http.FileSystem, file string) bool { 65 | if fs == nil { 66 | return false 67 | } 68 | 69 | f, err := fs.Open(file) 70 | if err != nil { 71 | return false 72 | } 73 | defer f.Close() 74 | 75 | stat, err := f.Stat() 76 | if err != nil { 77 | return false 78 | } 79 | 80 | // directory 81 | if strings.HasSuffix(file, "/") { 82 | return stat.IsDir() 83 | } 84 | 85 | // file 86 | return !stat.IsDir() 87 | } 88 | -------------------------------------------------------------------------------- /caddy/setup/templates.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/mholt/caddy/middleware" 7 | "github.com/mholt/caddy/middleware/templates" 8 | ) 9 | 10 | // Templates configures a new Templates middleware instance. 11 | func Templates(c *Controller) (middleware.Middleware, error) { 12 | rules, err := templatesParse(c) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | tmpls := templates.Templates{ 18 | Rules: rules, 19 | Root: c.Root, 20 | FileSys: http.Dir(c.Root), 21 | } 22 | 23 | return func(next middleware.Handler) middleware.Handler { 24 | tmpls.Next = next 25 | return tmpls 26 | }, nil 27 | } 28 | 29 | func templatesParse(c *Controller) ([]templates.Rule, error) { 30 | var rules []templates.Rule 31 | 32 | for c.Next() { 33 | var rule templates.Rule 34 | 35 | rule.Path = defaultTemplatePath 36 | rule.Extensions = defaultTemplateExtensions 37 | 38 | args := c.RemainingArgs() 39 | 40 | switch len(args) { 41 | case 0: 42 | // Optional block 43 | for c.NextBlock() { 44 | switch c.Val() { 45 | case "path": 46 | args := c.RemainingArgs() 47 | if len(args) != 1 { 48 | return nil, c.ArgErr() 49 | } 50 | rule.Path = args[0] 51 | 52 | case "ext": 53 | args := c.RemainingArgs() 54 | if len(args) == 0 { 55 | return nil, c.ArgErr() 56 | } 57 | rule.Extensions = args 58 | 59 | case "between": 60 | args := c.RemainingArgs() 61 | if len(args) != 2 { 62 | return nil, c.ArgErr() 63 | } 64 | rule.Delims[0] = args[0] 65 | rule.Delims[1] = args[1] 66 | } 67 | } 68 | default: 69 | // First argument would be the path 70 | rule.Path = args[0] 71 | 72 | // Any remaining arguments are extensions 73 | rule.Extensions = args[1:] 74 | if len(rule.Extensions) == 0 { 75 | rule.Extensions = defaultTemplateExtensions 76 | } 77 | } 78 | 79 | for _, ext := range rule.Extensions { 80 | rule.IndexFiles = append(rule.IndexFiles, "index"+ext) 81 | } 82 | 83 | rules = append(rules, rule) 84 | } 85 | return rules, nil 86 | } 87 | 88 | const defaultTemplatePath = "/" 89 | 90 | var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"} 91 | -------------------------------------------------------------------------------- /caddy/setup/websocket.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "github.com/mholt/caddy/middleware" 5 | "github.com/mholt/caddy/middleware/websocket" 6 | ) 7 | 8 | // WebSocket configures a new WebSocket middleware instance. 9 | func WebSocket(c *Controller) (middleware.Middleware, error) { 10 | 11 | websocks, err := webSocketParse(c) 12 | if err != nil { 13 | return nil, err 14 | } 15 | websocket.GatewayInterface = c.AppName + "-CGI/1.1" 16 | websocket.ServerSoftware = c.AppName + "/" + c.AppVersion 17 | 18 | return func(next middleware.Handler) middleware.Handler { 19 | return websocket.WebSocket{Next: next, Sockets: websocks} 20 | }, nil 21 | } 22 | 23 | func webSocketParse(c *Controller) ([]websocket.Config, error) { 24 | var websocks []websocket.Config 25 | var respawn bool 26 | 27 | optionalBlock := func() (hadBlock bool, err error) { 28 | for c.NextBlock() { 29 | hadBlock = true 30 | if c.Val() == "respawn" { 31 | respawn = true 32 | } else { 33 | return true, c.Err("Expected websocket configuration parameter in block") 34 | } 35 | } 36 | return 37 | } 38 | 39 | for c.Next() { 40 | var val, path, command string 41 | 42 | // Path or command; not sure which yet 43 | if !c.NextArg() { 44 | return nil, c.ArgErr() 45 | } 46 | val = c.Val() 47 | 48 | // Extra configuration may be in a block 49 | hadBlock, err := optionalBlock() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | if !hadBlock { 55 | // The next argument on this line will be the command or an open curly brace 56 | if c.NextArg() { 57 | path = val 58 | command = c.Val() 59 | } else { 60 | path = "/" 61 | command = val 62 | } 63 | 64 | // Okay, check again for optional block 65 | _, err = optionalBlock() 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | 71 | // Split command into the actual command and its arguments 72 | cmd, args, err := middleware.SplitCommandAndArgs(command) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | websocks = append(websocks, websocket.Config{ 78 | Path: path, 79 | Command: cmd, 80 | Arguments: args, 81 | Respawn: respawn, // TODO: This isn't used currently 82 | }) 83 | } 84 | 85 | return websocks, nil 86 | 87 | } 88 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 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 | 46 | func TestSetVersion(t *testing.T) { 47 | setVersion() 48 | if !devBuild { 49 | t.Error("Expected default to assume development build, but it didn't") 50 | } 51 | if got, want := appVersion, "(untracked dev build)"; got != want { 52 | t.Errorf("Expected appVersion='%s', got: '%s'", want, got) 53 | } 54 | 55 | gitTag = "v1.1" 56 | setVersion() 57 | if devBuild { 58 | t.Error("Expected a stable build if gitTag is set with no changes") 59 | } 60 | if got, want := appVersion, "1.1"; got != want { 61 | t.Errorf("Expected appVersion='%s', got: '%s'", want, got) 62 | } 63 | 64 | gitTag = "" 65 | gitNearestTag = "v1.0" 66 | gitCommit = "deadbeef" 67 | buildDate = "Fri Feb 26 06:53:17 UTC 2016" 68 | setVersion() 69 | if !devBuild { 70 | t.Error("Expected inferring a dev build when gitTag is empty") 71 | } 72 | if got, want := appVersion, "1.0 (+deadbeef Fri Feb 26 06:53:17 UTC 2016)"; got != want { 73 | t.Errorf("Expected appVersion='%s', got: '%s'", want, got) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /middleware/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 | } -------------------------------------------------------------------------------- /caddy/setup/gzip_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/gzip" 7 | ) 8 | 9 | func TestGzip(t *testing.T) { 10 | c := NewTestController(`gzip`) 11 | 12 | mid, err := Gzip(c) 13 | if err != nil { 14 | t.Errorf("Expected no errors, but got: %v", err) 15 | } 16 | if mid == nil { 17 | t.Fatal("Expected middleware, was nil instead") 18 | } 19 | 20 | handler := mid(EmptyNext) 21 | myHandler, ok := handler.(gzip.Gzip) 22 | if !ok { 23 | t.Fatalf("Expected handler to be type Gzip, got: %#v", handler) 24 | } 25 | 26 | if !SameNext(myHandler.Next, EmptyNext) { 27 | t.Error("'Next' field of handler was not set properly") 28 | } 29 | 30 | tests := []struct { 31 | input string 32 | shouldErr bool 33 | }{ 34 | {`gzip {`, true}, 35 | {`gzip {}`, true}, 36 | {`gzip a b`, true}, 37 | {`gzip a {`, true}, 38 | {`gzip { not f } `, true}, 39 | {`gzip { not } `, true}, 40 | {`gzip { not /file 41 | ext .html 42 | level 1 43 | } `, false}, 44 | {`gzip { level 9 } `, false}, 45 | {`gzip { ext } `, true}, 46 | {`gzip { ext /f 47 | } `, true}, 48 | {`gzip { not /file 49 | ext .html 50 | level 1 51 | } 52 | gzip`, false}, 53 | {`gzip { 54 | ext "" 55 | }`, false}, 56 | {`gzip { not /file 57 | ext .html 58 | level 1 59 | } 60 | gzip { not /file1 61 | ext .htm 62 | level 3 63 | } 64 | `, false}, 65 | {`gzip { not /file 66 | ext .html 67 | level 1 68 | } 69 | gzip { not /file1 70 | ext .htm 71 | level 3 72 | } 73 | `, false}, 74 | {`gzip { not /file 75 | ext * 76 | level 1 77 | } 78 | `, false}, 79 | {`gzip { not /file 80 | ext * 81 | level 1 82 | min_length ab 83 | } 84 | `, true}, 85 | {`gzip { not /file 86 | ext * 87 | level 1 88 | min_length 1000 89 | } 90 | `, false}, 91 | } 92 | for i, test := range tests { 93 | c := NewTestController(test.input) 94 | _, err := gzipParse(c) 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 | -------------------------------------------------------------------------------- /caddy/setup/headers_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/mholt/caddy/middleware/headers" 8 | ) 9 | 10 | func TestHeaders(t *testing.T) { 11 | c := NewTestController(`header / Foo Bar`) 12 | 13 | mid, err := Headers(c) 14 | if err != nil { 15 | t.Errorf("Expected no errors, but got: %v", err) 16 | } 17 | if mid == nil { 18 | t.Fatal("Expected middleware, was nil instead") 19 | } 20 | 21 | handler := mid(EmptyNext) 22 | myHandler, ok := handler.(headers.Headers) 23 | if !ok { 24 | t.Fatalf("Expected handler to be type Headers, got: %#v", handler) 25 | } 26 | 27 | if !SameNext(myHandler.Next, EmptyNext) { 28 | t.Error("'Next' field of handler was not set properly") 29 | } 30 | } 31 | 32 | func TestHeadersParse(t *testing.T) { 33 | tests := []struct { 34 | input string 35 | shouldErr bool 36 | expected []headers.Rule 37 | }{ 38 | {`header /foo Foo "Bar Baz"`, 39 | false, []headers.Rule{ 40 | {Path: "/foo", Headers: []headers.Header{ 41 | {Name: "Foo", Value: "Bar Baz"}, 42 | }}, 43 | }}, 44 | {`header /bar { Foo "Bar Baz" Baz Qux }`, 45 | false, []headers.Rule{ 46 | {Path: "/bar", Headers: []headers.Header{ 47 | {Name: "Foo", Value: "Bar Baz"}, 48 | {Name: "Baz", Value: "Qux"}, 49 | }}, 50 | }}, 51 | } 52 | 53 | for i, test := range tests { 54 | c := NewTestController(test.input) 55 | actual, err := headersParse(c) 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(actual) != len(test.expected) { 64 | t.Fatalf("Test %d expected %d rules, but got %d", 65 | i, len(test.expected), len(actual)) 66 | } 67 | 68 | for j, expectedRule := range test.expected { 69 | actualRule := actual[j] 70 | 71 | if actualRule.Path != expectedRule.Path { 72 | t.Errorf("Test %d, rule %d: Expected path %s, but got %s", 73 | i, j, expectedRule.Path, actualRule.Path) 74 | } 75 | 76 | expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers) 77 | actualHeaders := fmt.Sprintf("%v", actualRule.Headers) 78 | 79 | if actualHeaders != expectedHeaders { 80 | t.Errorf("Test %d, rule %d: Expected headers %s, but got %s", 81 | i, j, expectedHeaders, actualHeaders) 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /middleware/gzip/response_filter_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/middleware" 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 = middleware.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 | -------------------------------------------------------------------------------- /middleware/log/log.go: -------------------------------------------------------------------------------- 1 | // Package log implements request (access) logging middleware. 2 | package log 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/mholt/caddy/middleware" 10 | ) 11 | 12 | // Logger is a basic request logging middleware. 13 | type Logger struct { 14 | Next middleware.Handler 15 | Rules []Rule 16 | ErrorFunc func(http.ResponseWriter, *http.Request, int) // failover error handler 17 | } 18 | 19 | func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 20 | for _, rule := range l.Rules { 21 | if middleware.Path(r.URL.Path).Matches(rule.PathScope) { 22 | // Record the response 23 | responseRecorder := middleware.NewResponseRecorder(w) 24 | 25 | // Attach the Replacer we'll use so that other middlewares can 26 | // set their own placeholders if they want to. 27 | rep := middleware.NewReplacer(r, responseRecorder, CommonLogEmptyValue) 28 | responseRecorder.Replacer = rep 29 | 30 | // Bon voyage, request! 31 | status, err := l.Next.ServeHTTP(responseRecorder, r) 32 | 33 | if status >= 400 { 34 | // There was an error up the chain, but no response has been written yet. 35 | // The error must be handled here so the log entry will record the response size. 36 | if l.ErrorFunc != nil { 37 | l.ErrorFunc(responseRecorder, r, status) 38 | } else { 39 | // Default failover error handler 40 | responseRecorder.WriteHeader(status) 41 | fmt.Fprintf(responseRecorder, "%d %s", status, http.StatusText(status)) 42 | } 43 | status = 0 44 | } 45 | 46 | // Write log entry 47 | rule.Log.Println(rep.Replace(rule.Format)) 48 | 49 | return status, err 50 | } 51 | } 52 | return l.Next.ServeHTTP(w, r) 53 | } 54 | 55 | // Rule configures the logging middleware. 56 | type Rule struct { 57 | PathScope string 58 | OutputFile string 59 | Format string 60 | Log *log.Logger 61 | Roller *middleware.LogRoller 62 | } 63 | 64 | const ( 65 | // DefaultLogFilename is the default log filename. 66 | DefaultLogFilename = "access.log" 67 | // CommonLogFormat is the common log format. 68 | CommonLogFormat = `{remote} ` + CommonLogEmptyValue + ` [{when}] "{method} {uri} {proto}" {status} {size}` 69 | // CommonLogEmptyValue is the common empty log value. 70 | CommonLogEmptyValue = "-" 71 | // CombinedLogFormat is the combined log format. 72 | CombinedLogFormat = CommonLogFormat + ` "{>Referer}" "{>User-Agent}"` 73 | // DefaultLogFormat is the default log format. 74 | DefaultLogFormat = CommonLogFormat 75 | ) 76 | -------------------------------------------------------------------------------- /middleware/gzip/response_filter.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 | // ResponseFilterWriter validates ResponseFilters. It writes 29 | // gzip compressed data if ResponseFilters are satisfied or 30 | // uncompressed data otherwise. 31 | type ResponseFilterWriter struct { 32 | filters []ResponseFilter 33 | shouldCompress bool 34 | statusCodeWritten bool 35 | *gzipResponseWriter 36 | } 37 | 38 | // NewResponseFilterWriter creates and initializes a new ResponseFilterWriter. 39 | func NewResponseFilterWriter(filters []ResponseFilter, gz *gzipResponseWriter) *ResponseFilterWriter { 40 | return &ResponseFilterWriter{filters: filters, gzipResponseWriter: gz} 41 | } 42 | 43 | // WriteHeader wraps underlying WriteHeader method and 44 | // compresses if filters are satisfied. 45 | func (r *ResponseFilterWriter) WriteHeader(code int) { 46 | // Determine if compression should be used or not. 47 | r.shouldCompress = true 48 | for _, filter := range r.filters { 49 | if !filter.ShouldCompress(r) { 50 | r.shouldCompress = false 51 | break 52 | } 53 | } 54 | 55 | if r.shouldCompress { 56 | // replace discard writer with ResponseWriter 57 | if gzWriter, ok := r.gzipResponseWriter.Writer.(*gzip.Writer); ok { 58 | gzWriter.Reset(r.ResponseWriter) 59 | } 60 | // use gzip WriteHeader to include and delete 61 | // necessary headers 62 | r.gzipResponseWriter.WriteHeader(code) 63 | } else { 64 | r.ResponseWriter.WriteHeader(code) 65 | } 66 | r.statusCodeWritten = true 67 | } 68 | 69 | // Write wraps underlying Write method and compresses if filters 70 | // are satisfied 71 | func (r *ResponseFilterWriter) Write(b []byte) (int, error) { 72 | if !r.statusCodeWritten { 73 | r.WriteHeader(http.StatusOK) 74 | } 75 | if r.shouldCompress { 76 | return r.gzipResponseWriter.Write(b) 77 | } 78 | return r.ResponseWriter.Write(b) 79 | } 80 | -------------------------------------------------------------------------------- /middleware/proxy/policy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | var workableServer *httptest.Server 11 | 12 | func TestMain(m *testing.M) { 13 | workableServer = httptest.NewServer(http.HandlerFunc( 14 | func(w http.ResponseWriter, r *http.Request) { 15 | // do nothing 16 | })) 17 | r := m.Run() 18 | workableServer.Close() 19 | os.Exit(r) 20 | } 21 | 22 | type customPolicy struct{} 23 | 24 | func (r *customPolicy) Select(pool HostPool) *UpstreamHost { 25 | return pool[0] 26 | } 27 | 28 | func testPool() HostPool { 29 | pool := []*UpstreamHost{ 30 | { 31 | Name: workableServer.URL, // this should resolve (healthcheck test) 32 | }, 33 | { 34 | Name: "http://shouldnot.resolve", // this shouldn't 35 | }, 36 | { 37 | Name: "http://C", 38 | }, 39 | } 40 | return HostPool(pool) 41 | } 42 | 43 | func TestRoundRobinPolicy(t *testing.T) { 44 | pool := testPool() 45 | rrPolicy := &RoundRobin{} 46 | h := rrPolicy.Select(pool) 47 | // First selected host is 1, because counter starts at 0 48 | // and increments before host is selected 49 | if h != pool[1] { 50 | t.Error("Expected first round robin host to be second host in the pool.") 51 | } 52 | h = rrPolicy.Select(pool) 53 | if h != pool[2] { 54 | t.Error("Expected second round robin host to be third host in the pool.") 55 | } 56 | h = rrPolicy.Select(pool) 57 | if h != pool[0] { 58 | t.Error("Expected third round robin host to be first host in the pool.") 59 | } 60 | // mark host as down 61 | pool[1].Unhealthy = true 62 | h = rrPolicy.Select(pool) 63 | if h != pool[2] { 64 | t.Error("Expected to skip down host.") 65 | } 66 | // mark host as full 67 | pool[2].Conns = 1 68 | pool[2].MaxConns = 1 69 | h = rrPolicy.Select(pool) 70 | if h != pool[0] { 71 | t.Error("Expected to skip full host.") 72 | } 73 | } 74 | 75 | func TestLeastConnPolicy(t *testing.T) { 76 | pool := testPool() 77 | lcPolicy := &LeastConn{} 78 | pool[0].Conns = 10 79 | pool[1].Conns = 10 80 | h := lcPolicy.Select(pool) 81 | if h != pool[2] { 82 | t.Error("Expected least connection host to be third host.") 83 | } 84 | pool[2].Conns = 100 85 | h = lcPolicy.Select(pool) 86 | if h != pool[0] && h != pool[1] { 87 | t.Error("Expected least connection host to be first or second host.") 88 | } 89 | } 90 | 91 | func TestCustomPolicy(t *testing.T) { 92 | pool := testPool() 93 | customPolicy := &customPolicy{} 94 | h := customPolicy.Select(pool) 95 | if h != pool[0] { 96 | t.Error("Expected custom policy host to be the first host.") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /caddy/https/handshake_test.go: -------------------------------------------------------------------------------- 1 | package https 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 | hello := &tls.ClientHelloInfo{ServerName: "example.com"} 13 | helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"} 14 | helloNoSNI := &tls.ClientHelloInfo{} 15 | helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"} 16 | 17 | // When cache is empty 18 | if cert, err := GetCertificate(hello); err == nil { 19 | t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert) 20 | } 21 | if cert, err := GetCertificate(helloNoSNI); err == nil { 22 | t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert) 23 | } 24 | 25 | // When cache has one certificate in it (also is default) 26 | defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}} 27 | certCache[""] = defaultCert 28 | certCache["example.com"] = defaultCert 29 | if cert, err := GetCertificate(hello); err != nil { 30 | t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err) 31 | } else if cert.Leaf.DNSNames[0] != "example.com" { 32 | t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert) 33 | } 34 | if cert, err := GetCertificate(helloNoSNI); err != nil { 35 | t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err) 36 | } else if cert.Leaf.DNSNames[0] != "example.com" { 37 | t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert) 38 | } 39 | 40 | // When retrieving wildcard certificate 41 | certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}} 42 | if cert, err := GetCertificate(helloSub); err != nil { 43 | t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err) 44 | } else if cert.Leaf.DNSNames[0] != "*.example.com" { 45 | t.Errorf("Got wrong certificate, expected wildcard: %v", cert) 46 | } 47 | 48 | // When no certificate matches, the default is returned 49 | if cert, err := GetCertificate(helloNoMatch); err != nil { 50 | t.Errorf("Expected default certificate with no error when no matches, got err: %v", err) 51 | } else if cert.Leaf.DNSNames[0] != "example.com" { 52 | t.Errorf("Expected default cert with no matches, got: %v", cert) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dist/init/freebsd/caddy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | 4 | # PROVIDE: caddy 5 | # REQUIRE: LOGIN NETWORKING named cleanvar sshd 6 | # KEYWORD: shutdown 7 | 8 | # 9 | # Add the following lines to /etc/rc.conf to enable caddy: 10 | # caddy_enable (bool): Set to "NO" by default. 11 | # Set it to "YES" to enable caddy 12 | # 13 | # caddy_cert_email (str): Set to "" by default. 14 | # Defines the SSL certificate issuer email. By providing an 15 | # email address you automatically agree to letsencrypt.org's 16 | # general terms and conditions 17 | # 18 | # caddy_bin_path (str): Set to "/usr/local/bin/caddy" by default. 19 | # Provides the path to the caddy server executable 20 | # 21 | # caddy_cpu (str): Set to "99%" by default. 22 | # Configures, how much CPU capacity caddy may gain 23 | # 24 | # caddy_config_path (str): Set to "/usr/local/www/Caddyfile" by default. 25 | # Defines the path for the configuration file caddy will load on boot 26 | # 27 | # caddy_run_user (str): Set to "root" by default. 28 | # Defines the user that caddy will run on 29 | # 30 | 31 | . /etc/rc.subr 32 | 33 | name="caddy" 34 | rcvar=${name}_enable 35 | 36 | load_rc_config $name 37 | : ${caddy_enable:=no} 38 | : ${caddy_cert_email=""} 39 | : ${caddy_bin_path="/usr/local/bin/caddy"} 40 | : ${caddy_cpu="99%"} # was a bug for me that caused a crash within jails 41 | : ${caddy_config_path="/usr/local/www/Caddyfile"} 42 | : ${caddy_run_user="root"} 43 | 44 | if [ "$caddy_cert_email" = "" ] 45 | then 46 | echo "rc variable \$caddy_cert_email is not set. Please provide a valid SSL certificate issuer email." 47 | exit 1 48 | fi 49 | 50 | pidfile="/var/run/caddy.pid" 51 | logfile="/var/log/caddy.log" 52 | 53 | command="${caddy_bin_path} -log ${logfile} -pidfile ${pidfile} -cpu ${caddy_cpu} -conf ${caddy_config_path} -agree -email ${caddy_cert_email}" 54 | 55 | start_cmd="caddy_start" 56 | status_cmd="caddy_status" 57 | stop_cmd="caddy_stop" 58 | 59 | caddy_start() { 60 | echo "Starting Caddy..." 61 | /usr/sbin/daemon -u ${caddy_run_user} -c ${command} >> ${logfile} 62 | } 63 | 64 | caddy_status() { 65 | ps -p `cat ${pidfile} 2> /dev/null` > /dev/null 2>&1 && echo 'Running' || echo 'Not Running. Please note that upon startup it will take Caddy a few seconds to come up. So you might just wanna wait a few seconds before starting it again.' 66 | } 67 | 68 | caddy_stop() { 69 | echo "Stopping Caddy..." 70 | kill -QUIT `cat $pidfile 2> /dev/null` 71 | } 72 | 73 | run_rc_command "$1" 74 | -------------------------------------------------------------------------------- /middleware/templates/templates.go: -------------------------------------------------------------------------------- 1 | // Package templates implements template execution for files to be dynamically rendered for the client. 2 | package templates 3 | 4 | import ( 5 | "bytes" 6 | "net/http" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "text/template" 11 | 12 | "github.com/mholt/caddy/middleware" 13 | ) 14 | 15 | // ServeHTTP implements the middleware.Handler interface. 16 | func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 17 | for _, rule := range t.Rules { 18 | if !middleware.Path(r.URL.Path).Matches(rule.Path) { 19 | continue 20 | } 21 | 22 | // Check for index files 23 | fpath := r.URL.Path 24 | if idx, ok := middleware.IndexFile(t.FileSys, fpath, rule.IndexFiles); ok { 25 | fpath = idx 26 | } 27 | 28 | // Check the extension 29 | reqExt := path.Ext(fpath) 30 | 31 | for _, ext := range rule.Extensions { 32 | if reqExt == ext { 33 | // Create execution context 34 | ctx := middleware.Context{Root: t.FileSys, Req: r, URL: r.URL} 35 | 36 | // New template 37 | templateName := filepath.Base(fpath) 38 | tpl := template.New(templateName) 39 | 40 | // Set delims 41 | if rule.Delims != [2]string{} { 42 | tpl.Delims(rule.Delims[0], rule.Delims[1]) 43 | } 44 | 45 | // Build the template 46 | templatePath := filepath.Join(t.Root, fpath) 47 | tpl, err := tpl.ParseFiles(templatePath) 48 | if err != nil { 49 | if os.IsNotExist(err) { 50 | return http.StatusNotFound, nil 51 | } else if os.IsPermission(err) { 52 | return http.StatusForbidden, nil 53 | } 54 | return http.StatusInternalServerError, err 55 | } 56 | 57 | // Execute it 58 | var buf bytes.Buffer 59 | err = tpl.Execute(&buf, ctx) 60 | if err != nil { 61 | return http.StatusInternalServerError, err 62 | } 63 | 64 | templateInfo, err := os.Stat(templatePath) 65 | if err == nil { 66 | // add the Last-Modified header if we were able to read the stamp 67 | middleware.SetLastModifiedHeader(w, templateInfo.ModTime()) 68 | } 69 | buf.WriteTo(w) 70 | 71 | return http.StatusOK, nil 72 | } 73 | } 74 | } 75 | 76 | return t.Next.ServeHTTP(w, r) 77 | } 78 | 79 | // Templates is middleware to render templated files as the HTTP response. 80 | type Templates struct { 81 | Next middleware.Handler 82 | Rules []Rule 83 | Root string 84 | FileSys http.FileSystem 85 | } 86 | 87 | // Rule represents a template rule. A template will only execute 88 | // with this rule if the request path matches the Path specified 89 | // and requests a resource with one of the extensions specified. 90 | type Rule struct { 91 | Path string 92 | Extensions []string 93 | IndexFiles []string 94 | Delims [2]string 95 | } 96 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | 7 | "github.com/mholt/caddy/middleware" 8 | ) 9 | 10 | // Config configuration for a single server. 11 | type Config struct { 12 | // The hostname or IP on which to serve 13 | Host string 14 | 15 | // The host address to bind on - defaults to (virtual) Host if empty 16 | BindHost string 17 | 18 | // The port to listen on 19 | Port string 20 | 21 | // The protocol (http/https) to serve with this config; only set if user explicitly specifies it 22 | Scheme string 23 | 24 | // The directory from which to serve files 25 | Root string 26 | 27 | // HTTPS configuration 28 | TLS TLSConfig 29 | 30 | // Middleware stack 31 | Middleware []middleware.Middleware 32 | 33 | // Startup is a list of functions (or methods) to execute at 34 | // server startup and restart; these are executed before any 35 | // parts of the server are configured, and the functions are 36 | // blocking. These are good for setting up middlewares and 37 | // starting goroutines. 38 | Startup []func() error 39 | 40 | // FirstStartup is like Startup but these functions only execute 41 | // during the initial startup, not on subsequent restarts. 42 | // 43 | // (Note: The server does not ever run these on its own; it is up 44 | // to the calling application to do so, and do so only once, as the 45 | // server itself has no notion whether it's a restart or not.) 46 | FirstStartup []func() error 47 | 48 | // Functions (or methods) to execute when the server quits; 49 | // these are executed in response to SIGINT and are blocking 50 | Shutdown []func() error 51 | 52 | // The path to the configuration file from which this was loaded 53 | ConfigFile string 54 | 55 | // The name of the application 56 | AppName string 57 | 58 | // The application's version 59 | AppVersion string 60 | } 61 | 62 | // Address returns the host:port of c as a string. 63 | func (c Config) Address() string { 64 | return net.JoinHostPort(c.Host, c.Port) 65 | } 66 | 67 | // TLSConfig describes how TLS should be configured and used. 68 | type TLSConfig struct { 69 | Enabled bool // will be set to true if TLS is enabled 70 | LetsEncryptEmail string 71 | Manual bool // will be set to true if user provides own certs and keys 72 | Managed bool // will be set to true if config qualifies for implicit automatic/managed HTTPS 73 | OnDemand bool // will be set to true if user enables on-demand TLS (obtain certs during handshakes) 74 | Ciphers []uint16 75 | ProtocolMinVersion uint16 76 | ProtocolMaxVersion uint16 77 | PreferServerCipherSuites bool 78 | ClientCerts []string 79 | ClientAuth tls.ClientAuthType 80 | } 81 | -------------------------------------------------------------------------------- /middleware/gzip/request_filter.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/mholt/caddy/middleware" 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", ".md", ".xml", ".svg"} 19 | 20 | // DefaultExtFilter creates an ExtFilter with default extensions. 21 | func DefaultExtFilter() ExtFilter { 22 | m := ExtFilter{Exts: make(Set)} 23 | for _, extension := range defaultExtensions { 24 | m.Exts.Add(extension) 25 | } 26 | return m 27 | } 28 | 29 | // ExtFilter is RequestFilter for file name extensions. 30 | type ExtFilter struct { 31 | // Exts is the file name extensions to accept 32 | Exts Set 33 | } 34 | 35 | // ExtWildCard is the wildcard for extensions. 36 | const ExtWildCard = "*" 37 | 38 | // ShouldCompress checks if the request file extension matches any 39 | // of the registered extensions. It returns true if the extension is 40 | // found and false otherwise. 41 | func (e ExtFilter) ShouldCompress(r *http.Request) bool { 42 | ext := path.Ext(r.URL.Path) 43 | return e.Exts.Contains(ExtWildCard) || e.Exts.Contains(ext) 44 | } 45 | 46 | // PathFilter is RequestFilter for request path. 47 | type PathFilter struct { 48 | // IgnoredPaths is the paths to ignore 49 | IgnoredPaths Set 50 | } 51 | 52 | // ShouldCompress checks if the request path matches any of the 53 | // registered paths to ignore. It returns false if an ignored path 54 | // is found and true otherwise. 55 | func (p PathFilter) ShouldCompress(r *http.Request) bool { 56 | return !p.IgnoredPaths.ContainsFunc(func(value string) bool { 57 | return middleware.Path(r.URL.Path).Matches(value) 58 | }) 59 | } 60 | 61 | // Set stores distinct strings. 62 | type Set map[string]struct{} 63 | 64 | // Add adds an element to the set. 65 | func (s Set) Add(value string) { 66 | s[value] = struct{}{} 67 | } 68 | 69 | // Remove removes an element from the set. 70 | func (s Set) Remove(value string) { 71 | delete(s, value) 72 | } 73 | 74 | // Contains check if the set contains value. 75 | func (s Set) Contains(value string) bool { 76 | _, ok := s[value] 77 | return ok 78 | } 79 | 80 | // ContainsFunc is similar to Contains. It iterates all the 81 | // elements in the set and passes each to f. It returns true 82 | // on the first call to f that returns true and false otherwise. 83 | func (s Set) ContainsFunc(f func(string) bool) bool { 84 | for k := range s { 85 | if f(k) { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | -------------------------------------------------------------------------------- /caddy/parse/lexer.go: -------------------------------------------------------------------------------- 1 | package parse 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 | func (l *lexer) load(input io.Reader) error { 30 | l.reader = bufio.NewReader(input) 31 | l.line = 1 32 | return nil 33 | } 34 | 35 | // next loads the next token into the lexer. 36 | // A token is delimited by whitespace, unless 37 | // the token starts with a quotes character (") 38 | // in which case the token goes until the closing 39 | // quotes (the enclosing quotes are not included). 40 | // Inside quoted strings, quotes may be escaped 41 | // with a preceding \ character. No other chars 42 | // may be escaped. The rest of the line is skipped 43 | // if a "#" character is read in. Returns true if 44 | // a token was loaded; false otherwise. 45 | func (l *lexer) next() bool { 46 | var val []rune 47 | var comment, quoted, escaped bool 48 | 49 | makeToken := func() bool { 50 | l.token.text = string(val) 51 | return true 52 | } 53 | 54 | for { 55 | ch, _, err := l.reader.ReadRune() 56 | if err != nil { 57 | if len(val) > 0 { 58 | return makeToken() 59 | } 60 | if err == io.EOF { 61 | return false 62 | } 63 | panic(err) 64 | } 65 | 66 | if quoted { 67 | if !escaped { 68 | if ch == '\\' { 69 | escaped = true 70 | continue 71 | } else if ch == '"' { 72 | quoted = false 73 | return makeToken() 74 | } 75 | } 76 | if ch == '\n' { 77 | l.line++ 78 | } 79 | if escaped { 80 | // only escape quotes 81 | if ch != '"' { 82 | val = append(val, '\\') 83 | } 84 | } 85 | val = append(val, ch) 86 | escaped = false 87 | continue 88 | } 89 | 90 | if unicode.IsSpace(ch) { 91 | if ch == '\r' { 92 | continue 93 | } 94 | if ch == '\n' { 95 | l.line++ 96 | comment = false 97 | } 98 | if len(val) > 0 { 99 | return makeToken() 100 | } 101 | continue 102 | } 103 | 104 | if ch == '#' { 105 | comment = true 106 | } 107 | 108 | if comment { 109 | continue 110 | } 111 | 112 | if len(val) == 0 { 113 | l.token = token{line: l.line} 114 | if ch == '"' { 115 | quoted = true 116 | continue 117 | } 118 | } 119 | 120 | val = append(val, ch) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /middleware/inner/internal.go: -------------------------------------------------------------------------------- 1 | // Package inner 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 | package inner 5 | 6 | import ( 7 | "net/http" 8 | 9 | "github.com/mholt/caddy/middleware" 10 | ) 11 | 12 | // Internal middleware protects internal locations from external requests - 13 | // but allows access from the inside by using a special HTTP header. 14 | type Internal struct { 15 | Next middleware.Handler 16 | Paths []string 17 | } 18 | 19 | const ( 20 | redirectHeader string = "X-Accel-Redirect" 21 | maxRedirectCount int = 10 22 | ) 23 | 24 | func isInternalRedirect(w http.ResponseWriter) bool { 25 | return w.Header().Get(redirectHeader) != "" 26 | } 27 | 28 | // ServeHTTP implements the middlware.Handler interface. 29 | func (i Internal) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { 30 | 31 | // Internal location requested? -> Not found. 32 | for _, prefix := range i.Paths { 33 | if middleware.Path(r.URL.Path).Matches(prefix) { 34 | return http.StatusNotFound, nil 35 | } 36 | } 37 | 38 | // Use internal response writer to ignore responses that will be 39 | // redirected to internal locations 40 | iw := internalResponseWriter{ResponseWriter: w} 41 | status, err := i.Next.ServeHTTP(iw, r) 42 | 43 | for c := 0; c < maxRedirectCount && isInternalRedirect(iw); c++ { 44 | // Redirect - adapt request URL path and send it again 45 | // "down the chain" 46 | r.URL.Path = iw.Header().Get(redirectHeader) 47 | iw.ClearHeader() 48 | 49 | status, err = i.Next.ServeHTTP(iw, r) 50 | } 51 | 52 | if isInternalRedirect(iw) { 53 | // Too many redirect cycles 54 | iw.ClearHeader() 55 | return http.StatusInternalServerError, nil 56 | } 57 | 58 | return status, err 59 | } 60 | 61 | // internalResponseWriter wraps the underlying http.ResponseWriter and ignores 62 | // calls to Write and WriteHeader if the response should be redirected to an 63 | // internal location. 64 | type internalResponseWriter struct { 65 | http.ResponseWriter 66 | } 67 | 68 | // ClearHeader removes all header fields that are already set. 69 | func (w internalResponseWriter) ClearHeader() { 70 | for k := range w.Header() { 71 | w.Header().Del(k) 72 | } 73 | } 74 | 75 | // WriteHeader ignores the call if the response should be redirected to an 76 | // internal location. 77 | func (w internalResponseWriter) WriteHeader(code int) { 78 | if !isInternalRedirect(w) { 79 | w.ResponseWriter.WriteHeader(code) 80 | } 81 | } 82 | 83 | // Write ignores the call if the response should be redirected to an internal 84 | // location. 85 | func (w internalResponseWriter) Write(b []byte) (int, error) { 86 | if isInternalRedirect(w) { 87 | return 0, nil 88 | } 89 | return w.ResponseWriter.Write(b) 90 | } 91 | -------------------------------------------------------------------------------- /caddy/https/storage.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/mholt/caddy/caddy/assets" 8 | ) 9 | 10 | // storage is used to get file paths in a consistent, 11 | // cross-platform way for persisting Let's Encrypt assets 12 | // on the file system. 13 | var storage = Storage(filepath.Join(assets.Path(), "letsencrypt")) 14 | 15 | // Storage is a root directory and facilitates 16 | // forming file paths derived from it. 17 | type Storage string 18 | 19 | // Sites gets the directory that stores site certificate and keys. 20 | func (s Storage) Sites() string { 21 | return filepath.Join(string(s), "sites") 22 | } 23 | 24 | // Site returns the path to the folder containing assets for domain. 25 | func (s Storage) Site(domain string) string { 26 | return filepath.Join(s.Sites(), domain) 27 | } 28 | 29 | // SiteCertFile returns the path to the certificate file for domain. 30 | func (s Storage) SiteCertFile(domain string) string { 31 | return filepath.Join(s.Site(domain), domain+".crt") 32 | } 33 | 34 | // SiteKeyFile returns the path to domain's private key file. 35 | func (s Storage) SiteKeyFile(domain string) string { 36 | return filepath.Join(s.Site(domain), domain+".key") 37 | } 38 | 39 | // SiteMetaFile returns the path to the domain's asset metadata file. 40 | func (s Storage) SiteMetaFile(domain string) string { 41 | return filepath.Join(s.Site(domain), domain+".json") 42 | } 43 | 44 | // Users gets the directory that stores account folders. 45 | func (s Storage) Users() string { 46 | return filepath.Join(string(s), "users") 47 | } 48 | 49 | // User gets the account folder for the user with email. 50 | func (s Storage) User(email string) string { 51 | if email == "" { 52 | email = emptyEmail 53 | } 54 | return filepath.Join(s.Users(), email) 55 | } 56 | 57 | // UserRegFile gets the path to the registration file for 58 | // the user with the given email address. 59 | func (s Storage) UserRegFile(email string) string { 60 | if email == "" { 61 | email = emptyEmail 62 | } 63 | fileName := emailUsername(email) 64 | if fileName == "" { 65 | fileName = "registration" 66 | } 67 | return filepath.Join(s.User(email), fileName+".json") 68 | } 69 | 70 | // UserKeyFile gets the path to the private key file for 71 | // the user with the given email address. 72 | func (s Storage) UserKeyFile(email string) string { 73 | if email == "" { 74 | email = emptyEmail 75 | } 76 | fileName := emailUsername(email) 77 | if fileName == "" { 78 | fileName = "private" 79 | } 80 | return filepath.Join(s.User(email), fileName+".key") 81 | } 82 | 83 | // emailUsername returns the username portion of an 84 | // email address (part before '@') or the original 85 | // input if it can't find the "@" symbol. 86 | func emailUsername(email string) string { 87 | at := strings.Index(email, "@") 88 | if at == -1 { 89 | return email 90 | } else if at == 0 { 91 | return email[1:] 92 | } 93 | return email[:at] 94 | } 95 | -------------------------------------------------------------------------------- /caddy/setup/controller.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/mholt/caddy/caddy/parse" 9 | "github.com/mholt/caddy/middleware" 10 | "github.com/mholt/caddy/server" 11 | ) 12 | 13 | // Controller is given to the setup function of middlewares which 14 | // gives them access to be able to read tokens and set config. Each 15 | // virtualhost gets their own server config and dispenser. 16 | type Controller struct { 17 | *server.Config 18 | parse.Dispenser 19 | 20 | // OncePerServerBlock is a function that executes f 21 | // exactly once per server block, no matter how many 22 | // hosts are associated with it. If it is the first 23 | // time, the function f is executed immediately 24 | // (not deferred) and may return an error which is 25 | // returned by OncePerServerBlock. 26 | OncePerServerBlock func(f func() error) error 27 | 28 | // ServerBlockIndex is the 0-based index of the 29 | // server block as it appeared in the input. 30 | ServerBlockIndex int 31 | 32 | // ServerBlockHostIndex is the 0-based index of this 33 | // host as it appeared in the input at the head of the 34 | // server block. 35 | ServerBlockHostIndex int 36 | 37 | // ServerBlockHosts is a list of hosts that are 38 | // associated with this server block. All these 39 | // hosts, consequently, share the same tokens. 40 | ServerBlockHosts []string 41 | 42 | // ServerBlockStorage is used by a directive's 43 | // setup function to persist state between all 44 | // the hosts on a server block. 45 | ServerBlockStorage interface{} 46 | } 47 | 48 | // NewTestController creates a new *Controller for 49 | // the input specified, with a filename of "Testfile". 50 | // The Config is bare, consisting only of a Root of cwd. 51 | // 52 | // Used primarily for testing but needs to be exported so 53 | // add-ons can use this as a convenience. Does not initialize 54 | // the server-block-related fields. 55 | func NewTestController(input string) *Controller { 56 | return &Controller{ 57 | Config: &server.Config{ 58 | Root: ".", 59 | }, 60 | Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)), 61 | OncePerServerBlock: func(f func() error) error { 62 | return f() 63 | }, 64 | } 65 | } 66 | 67 | // EmptyNext is a no-op function that can be passed into 68 | // middleware.Middleware functions so that the assignment 69 | // to the Next field of the Handler can be tested. 70 | // 71 | // Used primarily for testing but needs to be exported so 72 | // add-ons can use this as a convenience. 73 | var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 74 | return 0, nil 75 | }) 76 | 77 | // SameNext does a pointer comparison between next1 and next2. 78 | // 79 | // Used primarily for testing but needs to be exported so 80 | // add-ons can use this as a convenience. 81 | func SameNext(next1, next2 middleware.Handler) bool { 82 | return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2) 83 | } 84 | -------------------------------------------------------------------------------- /caddy/https/certificates_test.go: -------------------------------------------------------------------------------- 1 | package https 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.exmaple.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 | -------------------------------------------------------------------------------- /caddy/setup/rewrite.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/mholt/caddy/middleware" 9 | "github.com/mholt/caddy/middleware/rewrite" 10 | ) 11 | 12 | // Rewrite configures a new Rewrite middleware instance. 13 | func Rewrite(c *Controller) (middleware.Middleware, error) { 14 | rewrites, err := rewriteParse(c) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return func(next middleware.Handler) middleware.Handler { 20 | return rewrite.Rewrite{ 21 | Next: next, 22 | FileSys: http.Dir(c.Root), 23 | Rules: rewrites, 24 | } 25 | }, nil 26 | } 27 | 28 | func rewriteParse(c *Controller) ([]rewrite.Rule, error) { 29 | var simpleRules []rewrite.Rule 30 | var regexpRules []rewrite.Rule 31 | 32 | for c.Next() { 33 | var rule rewrite.Rule 34 | var err error 35 | var base = "/" 36 | var pattern, to string 37 | var status int 38 | var ext []string 39 | 40 | args := c.RemainingArgs() 41 | 42 | var ifs []rewrite.If 43 | 44 | switch len(args) { 45 | case 1: 46 | base = args[0] 47 | fallthrough 48 | case 0: 49 | for c.NextBlock() { 50 | switch c.Val() { 51 | case "r", "regexp": 52 | if !c.NextArg() { 53 | return nil, c.ArgErr() 54 | } 55 | pattern = c.Val() 56 | case "to": 57 | args1 := c.RemainingArgs() 58 | if len(args1) == 0 { 59 | return nil, c.ArgErr() 60 | } 61 | to = strings.Join(args1, " ") 62 | case "ext": 63 | args1 := c.RemainingArgs() 64 | if len(args1) == 0 { 65 | return nil, c.ArgErr() 66 | } 67 | ext = args1 68 | case "if": 69 | args1 := c.RemainingArgs() 70 | if len(args1) != 3 { 71 | return nil, c.ArgErr() 72 | } 73 | ifCond, err := rewrite.NewIf(args1[0], args1[1], args1[2]) 74 | if err != nil { 75 | return nil, err 76 | } 77 | ifs = append(ifs, ifCond) 78 | case "status": 79 | if !c.NextArg() { 80 | return nil, c.ArgErr() 81 | } 82 | status, _ = strconv.Atoi(c.Val()) 83 | if status < 200 || (status > 299 && status < 400) || status > 499 { 84 | return nil, c.Err("status must be 2xx or 4xx") 85 | } 86 | default: 87 | return nil, c.ArgErr() 88 | } 89 | } 90 | // ensure to or status is specified 91 | if to == "" && status == 0 { 92 | return nil, c.ArgErr() 93 | } 94 | if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil { 95 | return nil, err 96 | } 97 | regexpRules = append(regexpRules, rule) 98 | 99 | // the only unhandled case is 2 and above 100 | default: 101 | rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " ")) 102 | simpleRules = append(simpleRules, rule) 103 | } 104 | 105 | } 106 | 107 | // put simple rules in front to avoid regexp computation for them 108 | return append(simpleRules, regexpRules...), nil 109 | } 110 | -------------------------------------------------------------------------------- /middleware/gzip/request_filter_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 | -------------------------------------------------------------------------------- /middleware/proxy/policy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | ) 7 | 8 | // HostPool is a collection of UpstreamHosts. 9 | type HostPool []*UpstreamHost 10 | 11 | // Policy decides how a host will be selected from a pool. 12 | type Policy interface { 13 | Select(pool HostPool) *UpstreamHost 14 | } 15 | 16 | func init() { 17 | RegisterPolicy("random", func() Policy { return &Random{} }) 18 | RegisterPolicy("least_conn", func() Policy { return &LeastConn{} }) 19 | RegisterPolicy("round_robin", func() Policy { return &RoundRobin{} }) 20 | } 21 | 22 | // Random is a policy that selects up hosts from a pool at random. 23 | type Random struct{} 24 | 25 | // Select selects an up host at random from the specified pool. 26 | func (r *Random) Select(pool HostPool) *UpstreamHost { 27 | // instead of just generating a random index 28 | // this is done to prevent selecting a unavailable host 29 | var randHost *UpstreamHost 30 | count := 0 31 | for _, host := range pool { 32 | if !host.Available() { 33 | continue 34 | } 35 | count++ 36 | if count == 1 { 37 | randHost = host 38 | } else { 39 | r := rand.Int() % count 40 | if r == (count - 1) { 41 | randHost = host 42 | } 43 | } 44 | } 45 | return randHost 46 | } 47 | 48 | // LeastConn is a policy that selects the host with the least connections. 49 | type LeastConn struct{} 50 | 51 | // Select selects the up host with the least number of connections in the 52 | // pool. If more than one host has the same least number of connections, 53 | // one of the hosts is chosen at random. 54 | func (r *LeastConn) Select(pool HostPool) *UpstreamHost { 55 | var bestHost *UpstreamHost 56 | count := 0 57 | leastConn := int64(1<<63 - 1) 58 | for _, host := range pool { 59 | if !host.Available() { 60 | continue 61 | } 62 | hostConns := host.Conns 63 | if hostConns < leastConn { 64 | bestHost = host 65 | leastConn = hostConns 66 | count = 1 67 | } else if hostConns == leastConn { 68 | // randomly select host among hosts with least connections 69 | count++ 70 | if count == 1 { 71 | bestHost = host 72 | } else { 73 | r := rand.Int() % count 74 | if r == (count - 1) { 75 | bestHost = host 76 | } 77 | } 78 | } 79 | } 80 | return bestHost 81 | } 82 | 83 | // RoundRobin is a policy that selects hosts based on round robin ordering. 84 | type RoundRobin struct { 85 | Robin uint32 86 | } 87 | 88 | // Select selects an up host from the pool using a round robin ordering scheme. 89 | func (r *RoundRobin) Select(pool HostPool) *UpstreamHost { 90 | poolLen := uint32(len(pool)) 91 | selection := atomic.AddUint32(&r.Robin, 1) % poolLen 92 | host := pool[selection] 93 | // if the currently selected host is not available, just ffwd to up host 94 | for i := uint32(1); !host.Available() && i < poolLen; i++ { 95 | host = pool[(selection+i)%poolLen] 96 | } 97 | if !host.Available() { 98 | return nil 99 | } 100 | return host 101 | } 102 | -------------------------------------------------------------------------------- /caddy/setup/fastcgi.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "path/filepath" 7 | 8 | "github.com/mholt/caddy/middleware" 9 | "github.com/mholt/caddy/middleware/fastcgi" 10 | ) 11 | 12 | // FastCGI configures a new FastCGI middleware instance. 13 | func FastCGI(c *Controller) (middleware.Middleware, error) { 14 | absRoot, err := filepath.Abs(c.Root) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | rules, err := fastcgiParse(c) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return func(next middleware.Handler) middleware.Handler { 25 | return fastcgi.Handler{ 26 | Next: next, 27 | Rules: rules, 28 | Root: c.Root, 29 | AbsRoot: absRoot, 30 | FileSys: http.Dir(c.Root), 31 | SoftwareName: c.AppName, 32 | SoftwareVersion: c.AppVersion, 33 | ServerName: c.Host, 34 | ServerPort: c.Port, 35 | } 36 | }, nil 37 | } 38 | 39 | func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) { 40 | var rules []fastcgi.Rule 41 | 42 | for c.Next() { 43 | var rule fastcgi.Rule 44 | 45 | args := c.RemainingArgs() 46 | 47 | switch len(args) { 48 | case 0: 49 | return rules, c.ArgErr() 50 | case 1: 51 | rule.Path = "/" 52 | rule.Address = args[0] 53 | case 2: 54 | rule.Path = args[0] 55 | rule.Address = args[1] 56 | case 3: 57 | rule.Path = args[0] 58 | rule.Address = args[1] 59 | err := fastcgiPreset(args[2], &rule) 60 | if err != nil { 61 | return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'") 62 | } 63 | } 64 | 65 | for c.NextBlock() { 66 | switch c.Val() { 67 | case "ext": 68 | if !c.NextArg() { 69 | return rules, c.ArgErr() 70 | } 71 | rule.Ext = c.Val() 72 | case "split": 73 | if !c.NextArg() { 74 | return rules, c.ArgErr() 75 | } 76 | rule.SplitPath = c.Val() 77 | case "index": 78 | args := c.RemainingArgs() 79 | if len(args) == 0 { 80 | return rules, c.ArgErr() 81 | } 82 | rule.IndexFiles = args 83 | case "env": 84 | envArgs := c.RemainingArgs() 85 | if len(envArgs) < 2 { 86 | return rules, c.ArgErr() 87 | } 88 | rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]}) 89 | case "except": 90 | ignoredPaths := c.RemainingArgs() 91 | if len(ignoredPaths) == 0 { 92 | return rules, c.ArgErr() 93 | } 94 | rule.IgnoredSubPaths = ignoredPaths 95 | } 96 | } 97 | 98 | rules = append(rules, rule) 99 | } 100 | 101 | return rules, nil 102 | } 103 | 104 | // fastcgiPreset configures rule according to name. It returns an error if 105 | // name is not a recognized preset name. 106 | func fastcgiPreset(name string, rule *fastcgi.Rule) error { 107 | switch name { 108 | case "php": 109 | rule.Ext = ".php" 110 | rule.SplitPath = ".php" 111 | rule.IndexFiles = []string{"index.php"} 112 | default: 113 | return errors.New(name + " is not a valid preset name") 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /middleware/rewrite/condition_test.go: -------------------------------------------------------------------------------- 1 | package rewrite 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestConditions(t *testing.T) { 10 | tests := []struct { 11 | condition string 12 | isTrue bool 13 | }{ 14 | {"a is b", false}, 15 | {"a is a", true}, 16 | {"a not b", true}, 17 | {"a not a", false}, 18 | {"a has a", true}, 19 | {"a has b", false}, 20 | {"ba has b", true}, 21 | {"bab has b", true}, 22 | {"bab has bb", false}, 23 | {"a not_has a", false}, 24 | {"a not_has b", true}, 25 | {"ba not_has b", false}, 26 | {"bab not_has b", false}, 27 | {"bab not_has bb", true}, 28 | {"bab starts_with bb", false}, 29 | {"bab starts_with ba", true}, 30 | {"bab starts_with bab", true}, 31 | {"bab ends_with bb", false}, 32 | {"bab ends_with bab", true}, 33 | {"bab ends_with ab", true}, 34 | {"a match *", false}, 35 | {"a match a", true}, 36 | {"a match .*", true}, 37 | {"a match a.*", true}, 38 | {"a match b.*", false}, 39 | {"ba match b.*", true}, 40 | {"ba match b[a-z]", true}, 41 | {"b0 match b[a-z]", false}, 42 | {"b0a match b[a-z]", false}, 43 | {"b0a match b[a-z]+", false}, 44 | {"b0a match b[a-z0-9]+", true}, 45 | {"a not_match *", true}, 46 | {"a not_match a", false}, 47 | {"a not_match .*", false}, 48 | {"a not_match a.*", false}, 49 | {"a not_match b.*", true}, 50 | {"ba not_match b.*", false}, 51 | {"ba not_match b[a-z]", false}, 52 | {"b0 not_match b[a-z]", true}, 53 | {"b0a not_match b[a-z]", true}, 54 | {"b0a not_match b[a-z]+", true}, 55 | {"b0a not_match b[a-z0-9]+", false}, 56 | } 57 | 58 | for i, test := range tests { 59 | str := strings.Fields(test.condition) 60 | ifCond, err := NewIf(str[0], str[1], str[2]) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | isTrue := ifCond.True(nil) 65 | if isTrue != test.isTrue { 66 | t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue) 67 | } 68 | } 69 | 70 | invalidOperators := []string{"ss", "and", "if"} 71 | for _, op := range invalidOperators { 72 | _, err := NewIf("a", op, "b") 73 | if err == nil { 74 | t.Errorf("Invalid operator %v used, expected error.", op) 75 | } 76 | } 77 | 78 | replaceTests := []struct { 79 | url string 80 | condition string 81 | isTrue bool 82 | }{ 83 | {"/home", "{uri} match /home", true}, 84 | {"/hom", "{uri} match /home", false}, 85 | {"/hom", "{uri} starts_with /home", false}, 86 | {"/hom", "{uri} starts_with /h", true}, 87 | {"/home/.hiddenfile", `{uri} match \/\.(.*)`, true}, 88 | {"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true}, 89 | } 90 | 91 | for i, test := range replaceTests { 92 | r, err := http.NewRequest("GET", test.url, nil) 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | str := strings.Fields(test.condition) 97 | ifCond, err := NewIf(str[0], str[1], str[2]) 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | isTrue := ifCond.True(r) 102 | if isTrue != test.isTrue { 103 | t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /caddy/setup/websocket_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/websocket" 7 | ) 8 | 9 | func TestWebSocket(t *testing.T) { 10 | 11 | c := NewTestController(`websocket cat`) 12 | 13 | mid, err := WebSocket(c) 14 | 15 | if err != nil { 16 | t.Errorf("Expected no errors, got: %v", err) 17 | } 18 | 19 | if mid == nil { 20 | t.Fatal("Expected middleware, was nil instead") 21 | } 22 | 23 | handler := mid(EmptyNext) 24 | myHandler, ok := handler.(websocket.WebSocket) 25 | 26 | if !ok { 27 | t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler) 28 | } 29 | 30 | if myHandler.Sockets[0].Path != "/" { 31 | t.Errorf("Expected / as the default Path") 32 | } 33 | if myHandler.Sockets[0].Command != "cat" { 34 | t.Errorf("Expected %s as the command", "cat") 35 | } 36 | 37 | } 38 | func TestWebSocketParse(t *testing.T) { 39 | tests := []struct { 40 | inputWebSocketConfig string 41 | shouldErr bool 42 | expectedWebSocketConfig []websocket.Config 43 | }{ 44 | {`websocket /api1 cat`, false, []websocket.Config{{ 45 | Path: "/api1", 46 | Command: "cat", 47 | }}}, 48 | 49 | {`websocket /api3 cat 50 | websocket /api4 cat `, false, []websocket.Config{{ 51 | Path: "/api3", 52 | Command: "cat", 53 | }, { 54 | Path: "/api4", 55 | Command: "cat", 56 | }}}, 57 | 58 | {`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{ 59 | Path: "/api5", 60 | Command: "cmd", 61 | Arguments: []string{"arg1", "arg2", "arg3"}, 62 | }}}, 63 | 64 | // accept respawn 65 | {`websocket /api6 cat { 66 | respawn 67 | }`, false, []websocket.Config{{ 68 | Path: "/api6", 69 | Command: "cat", 70 | }}}, 71 | 72 | // invalid configuration 73 | {`websocket /api7 cat { 74 | invalid 75 | }`, true, []websocket.Config{}}, 76 | } 77 | for i, test := range tests { 78 | c := NewTestController(test.inputWebSocketConfig) 79 | actualWebSocketConfigs, err := webSocketParse(c) 80 | 81 | if err == nil && test.shouldErr { 82 | t.Errorf("Test %d didn't error, but it should have", i) 83 | } else if err != nil && !test.shouldErr { 84 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 85 | } 86 | if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) { 87 | t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ", 88 | i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs)) 89 | } 90 | for j, actualWebSocketConfig := range actualWebSocketConfigs { 91 | 92 | if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path { 93 | t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s", 94 | i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path) 95 | } 96 | 97 | if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command { 98 | t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s", 99 | i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command) 100 | } 101 | 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /middleware/recorder.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | // ResponseRecorder is a type of http.ResponseWriter that captures 12 | // the status code written to it and also the size of the body 13 | // written in the response. A status code does not have 14 | // to be written, however, in which case 200 must be assumed. 15 | // It is best to have the constructor initialize this type 16 | // with that default status code. 17 | // 18 | // Setting the Replacer field allows middlewares to type-assert 19 | // the http.ResponseWriter to ResponseRecorder and set their own 20 | // placeholder values for logging utilities to use. 21 | // 22 | // Beware when accessing the Replacer value; it may be nil! 23 | type ResponseRecorder struct { 24 | http.ResponseWriter 25 | Replacer Replacer 26 | status int 27 | size int 28 | start time.Time 29 | } 30 | 31 | // NewResponseRecorder makes and returns a new responseRecorder, 32 | // which captures the HTTP Status code from the ResponseWriter 33 | // and also the length of the response body written through it. 34 | // Because a status is not set unless WriteHeader is called 35 | // explicitly, this constructor initializes with a status code 36 | // of 200 to cover the default case. 37 | func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder { 38 | return &ResponseRecorder{ 39 | ResponseWriter: w, 40 | status: http.StatusOK, 41 | start: time.Now(), 42 | } 43 | } 44 | 45 | // WriteHeader records the status code and calls the 46 | // underlying ResponseWriter's WriteHeader method. 47 | func (r *ResponseRecorder) WriteHeader(status int) { 48 | r.status = status 49 | r.ResponseWriter.WriteHeader(status) 50 | } 51 | 52 | // Write is a wrapper that records the size of the body 53 | // that gets written. 54 | func (r *ResponseRecorder) Write(buf []byte) (int, error) { 55 | n, err := r.ResponseWriter.Write(buf) 56 | if err == nil { 57 | r.size += n 58 | } 59 | return n, err 60 | } 61 | 62 | // Size is a Getter to size property 63 | func (r *ResponseRecorder) Size() int { 64 | return r.size 65 | } 66 | 67 | // Status is a Getter to status property 68 | func (r *ResponseRecorder) Status() int { 69 | return r.status 70 | } 71 | 72 | // Hijack implements http.Hijacker. It simply wraps the underlying 73 | // ResponseWriter's Hijack method if there is one, or returns an error. 74 | func (r *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) { 75 | if hj, ok := r.ResponseWriter.(http.Hijacker); ok { 76 | return hj.Hijack() 77 | } 78 | return nil, nil, errors.New("not a Hijacker") 79 | } 80 | 81 | // Flush implements http.Flusher. It simply wraps the underlying 82 | // ResponseWriter's Flush method if there is one, or does nothing. 83 | func (r *ResponseRecorder) Flush() { 84 | if f, ok := r.ResponseWriter.(http.Flusher); ok { 85 | f.Flush() 86 | } else { 87 | panic("not a Flusher") // should be recovered at the beginning of middleware stack 88 | } 89 | } 90 | 91 | // CloseNotify implements http.CloseNotifier. 92 | // It just inherits the underlying ResponseWriter's CloseNotify method. 93 | func (r *ResponseRecorder) CloseNotify() <-chan bool { 94 | if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok { 95 | return cn.CloseNotify() 96 | } 97 | panic("not a CloseNotifier") 98 | } 99 | -------------------------------------------------------------------------------- /middleware/commands.go: -------------------------------------------------------------------------------- 1 | package middleware 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 14 | // shell-style into the 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 | -------------------------------------------------------------------------------- /caddy/setup/root_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRoot(t *testing.T) { 13 | 14 | // Predefined error substrings 15 | parseErrContent := "Parse error:" 16 | unableToAccessErrContent := "Unable to access root path" 17 | 18 | existingDirPath, err := getTempDirPath() 19 | if err != nil { 20 | t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err) 21 | } 22 | 23 | nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir") 24 | 25 | existingFile, err := ioutil.TempFile("", "root_test") 26 | if err != nil { 27 | t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err) 28 | } 29 | defer func() { 30 | existingFile.Close() 31 | os.Remove(existingFile.Name()) 32 | }() 33 | 34 | inaccessiblePath := getInaccessiblePath(existingFile.Name()) 35 | 36 | tests := []struct { 37 | input string 38 | shouldErr bool 39 | expectedRoot string // expected root, set to the controller. Empty for negative cases. 40 | expectedErrContent string // substring from the expected error. Empty for positive cases. 41 | }{ 42 | // positive 43 | { 44 | fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "", 45 | }, 46 | { 47 | fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "", 48 | }, 49 | // negative 50 | { 51 | `root `, true, "", parseErrContent, 52 | }, 53 | { 54 | fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent, 55 | }, 56 | { 57 | fmt.Sprintf(`root { 58 | %s 59 | }`, existingDirPath), true, "", parseErrContent, 60 | }, 61 | } 62 | 63 | for i, test := range tests { 64 | c := NewTestController(test.input) 65 | mid, err := Root(c) 66 | 67 | if test.shouldErr && err == nil { 68 | t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) 69 | } 70 | 71 | if err != nil { 72 | if !test.shouldErr { 73 | t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) 74 | } 75 | 76 | if !strings.Contains(err.Error(), test.expectedErrContent) { 77 | t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) 78 | } 79 | } 80 | 81 | // the Root method always returns a nil middleware 82 | if mid != nil { 83 | t.Errorf("Middware, returned from Root() was not nil: %v", mid) 84 | } 85 | 86 | // check c.Root only if we are in a positive test. 87 | if !test.shouldErr && test.expectedRoot != c.Root { 88 | t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root) 89 | } 90 | } 91 | } 92 | 93 | // getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned. 94 | func getTempDirPath() (string, error) { 95 | tempDir := os.TempDir() 96 | 97 | _, err := os.Stat(tempDir) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | return tempDir, nil 103 | } 104 | 105 | func getInaccessiblePath(file string) string { 106 | // null byte in filename is not allowed on Windows AND unix 107 | return filepath.Join("C:", "file\x00name") 108 | } 109 | -------------------------------------------------------------------------------- /caddy/https/crypto_test.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/ecdsa" 7 | "crypto/elliptic" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/x509" 11 | "errors" 12 | "os" 13 | "runtime" 14 | "testing" 15 | ) 16 | 17 | func TestSaveAndLoadRSAPrivateKey(t *testing.T) { 18 | keyFile := "test.key" 19 | defer os.Remove(keyFile) 20 | 21 | privateKey, err := rsa.GenerateKey(rand.Reader, 128) // make tests faster; small key size OK for testing 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | // test save 27 | err = savePrivateKey(privateKey, keyFile) 28 | if err != nil { 29 | t.Fatal("error saving private key:", err) 30 | } 31 | 32 | // it doesn't make sense to test file permission on windows 33 | if runtime.GOOS != "windows" { 34 | // get info of the key file 35 | info, err := os.Stat(keyFile) 36 | if err != nil { 37 | t.Fatal("error stating private key:", err) 38 | } 39 | // verify permission of key file is correct 40 | if info.Mode().Perm() != 0600 { 41 | t.Error("Expected key file to have permission 0600, but it wasn't") 42 | } 43 | } 44 | 45 | // test load 46 | loadedKey, err := loadPrivateKey(keyFile) 47 | if err != nil { 48 | t.Error("error loading private key:", err) 49 | } 50 | 51 | // verify loaded key is correct 52 | if !PrivateKeysSame(privateKey, loadedKey) { 53 | t.Error("Expected key bytes to be the same, but they weren't") 54 | } 55 | } 56 | 57 | func TestSaveAndLoadECCPrivateKey(t *testing.T) { 58 | keyFile := "test.key" 59 | defer os.Remove(keyFile) 60 | 61 | privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | // test save 67 | err = savePrivateKey(privateKey, keyFile) 68 | if err != nil { 69 | t.Fatal("error saving private key:", err) 70 | } 71 | 72 | // it doesn't make sense to test file permission on windows 73 | if runtime.GOOS != "windows" { 74 | // get info of the key file 75 | info, err := os.Stat(keyFile) 76 | if err != nil { 77 | t.Fatal("error stating private key:", err) 78 | } 79 | // verify permission of key file is correct 80 | if info.Mode().Perm() != 0600 { 81 | t.Error("Expected key file to have permission 0600, but it wasn't") 82 | } 83 | } 84 | 85 | // test load 86 | loadedKey, err := loadPrivateKey(keyFile) 87 | if err != nil { 88 | t.Error("error loading private key:", err) 89 | } 90 | 91 | // verify loaded key is correct 92 | if !PrivateKeysSame(privateKey, loadedKey) { 93 | t.Error("Expected key bytes to be the same, but they weren't") 94 | } 95 | } 96 | 97 | // PrivateKeysSame compares the bytes of a and b and returns true if they are the same. 98 | func PrivateKeysSame(a, b crypto.PrivateKey) bool { 99 | var abytes, bbytes []byte 100 | var err error 101 | 102 | if abytes, err = PrivateKeyBytes(a); err != nil { 103 | return false 104 | } 105 | if bbytes, err = PrivateKeyBytes(b); err != nil { 106 | return false 107 | } 108 | return bytes.Equal(abytes, bbytes) 109 | } 110 | 111 | // PrivateKeyBytes returns the bytes of DER-encoded key. 112 | func PrivateKeyBytes(key crypto.PrivateKey) ([]byte, error) { 113 | switch key := key.(type) { 114 | case *rsa.PrivateKey: 115 | return x509.MarshalPKCS1PrivateKey(key), nil 116 | case *ecdsa.PrivateKey: 117 | return x509.MarshalECPrivateKey(key) 118 | } 119 | return nil, errors.New("Unknown private key type") 120 | } 121 | -------------------------------------------------------------------------------- /caddy/setup/redir_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mholt/caddy/middleware/redirect" 7 | ) 8 | 9 | func TestRedir(t *testing.T) { 10 | 11 | for j, test := range []struct { 12 | input string 13 | shouldErr bool 14 | expectedRules []redirect.Rule 15 | }{ 16 | // test case #0 tests the recognition of a valid HTTP status code defined outside of block statement 17 | {"redir 300 {\n/ /foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 300}}}, 18 | 19 | // test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement 20 | {"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{{}}}, 21 | 22 | // test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement 23 | {"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{{}}}, 24 | 25 | // test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement 26 | {"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{{}}}, 27 | 28 | // test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently 29 | {"redir 302 {\n/foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 302}}}, 30 | 31 | // test case #5 tests the recognition of a TO and From redirection in a block statement 32 | {"redir {\n/bar /foo 303\n}", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}}, 33 | 34 | // test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently 35 | {"redir /foo", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 301}}}, 36 | 37 | // test case #7 tests the recognition of a TO and From redirection in a non-block statement 38 | {"redir /bar /foo 303", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}}, 39 | 40 | // test case #8 tests the recognition of multiple redirections 41 | {"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}}, 42 | 43 | // test case #9 tests the detection of duplicate redirections 44 | {"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{{}}}, 45 | } { 46 | recievedFunc, err := Redir(NewTestController(test.input)) 47 | if err != nil && !test.shouldErr { 48 | t.Errorf("Test case #%d recieved an error of %v", j, err) 49 | } else if test.shouldErr { 50 | continue 51 | } 52 | recievedRules := recievedFunc(nil).(redirect.Redirect).Rules 53 | 54 | for i, recievedRule := range recievedRules { 55 | if recievedRule.FromPath != test.expectedRules[i].FromPath { 56 | t.Errorf("Test case #%d.%d expected a from path of %s, but recieved a from path of %s", j, i, test.expectedRules[i].FromPath, recievedRule.FromPath) 57 | } 58 | if recievedRule.To != test.expectedRules[i].To { 59 | t.Errorf("Test case #%d.%d expected a TO path of %s, but recieved a TO path of %s", j, i, test.expectedRules[i].To, recievedRule.To) 60 | } 61 | if recievedRule.Code != test.expectedRules[i].Code { 62 | t.Errorf("Test case #%d.%d expected a HTTP status code of %d, but recieved a code of %d", j, i, test.expectedRules[i].Code, recievedRule.Code) 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /caddy/setup/gzip.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/mholt/caddy/middleware" 9 | "github.com/mholt/caddy/middleware/gzip" 10 | ) 11 | 12 | // Gzip configures a new gzip middleware instance. 13 | func Gzip(c *Controller) (middleware.Middleware, error) { 14 | configs, err := gzipParse(c) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return func(next middleware.Handler) middleware.Handler { 20 | return gzip.Gzip{Next: next, Configs: configs} 21 | }, nil 22 | } 23 | 24 | func gzipParse(c *Controller) ([]gzip.Config, error) { 25 | var configs []gzip.Config 26 | 27 | for c.Next() { 28 | config := gzip.Config{} 29 | 30 | // Request Filters 31 | pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)} 32 | extFilter := gzip.ExtFilter{Exts: make(gzip.Set)} 33 | 34 | // Response Filters 35 | lengthFilter := gzip.LengthFilter(0) 36 | 37 | // No extra args expected 38 | if len(c.RemainingArgs()) > 0 { 39 | return configs, c.ArgErr() 40 | } 41 | 42 | for c.NextBlock() { 43 | switch c.Val() { 44 | case "ext": 45 | exts := c.RemainingArgs() 46 | if len(exts) == 0 { 47 | return configs, c.ArgErr() 48 | } 49 | for _, e := range exts { 50 | if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard && e != "" { 51 | return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e) 52 | } 53 | extFilter.Exts.Add(e) 54 | } 55 | case "not": 56 | paths := c.RemainingArgs() 57 | if len(paths) == 0 { 58 | return configs, c.ArgErr() 59 | } 60 | for _, p := range paths { 61 | if p == "/" { 62 | return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`) 63 | } 64 | if !strings.HasPrefix(p, "/") { 65 | return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p) 66 | } 67 | pathFilter.IgnoredPaths.Add(p) 68 | } 69 | case "level": 70 | if !c.NextArg() { 71 | return configs, c.ArgErr() 72 | } 73 | level, _ := strconv.Atoi(c.Val()) 74 | config.Level = level 75 | case "min_length": 76 | if !c.NextArg() { 77 | return configs, c.ArgErr() 78 | } 79 | length, err := strconv.ParseInt(c.Val(), 10, 64) 80 | if err != nil { 81 | return configs, err 82 | } else if length == 0 { 83 | return configs, fmt.Errorf(`gzip: min_length must be greater than 0`) 84 | } 85 | lengthFilter = gzip.LengthFilter(length) 86 | default: 87 | return configs, c.ArgErr() 88 | } 89 | } 90 | 91 | // Request Filters 92 | config.RequestFilters = []gzip.RequestFilter{} 93 | 94 | // If ignored paths are specified, put in front to filter with path first 95 | if len(pathFilter.IgnoredPaths) > 0 { 96 | config.RequestFilters = []gzip.RequestFilter{pathFilter} 97 | } 98 | 99 | // Then, if extensions are specified, use those to filter. 100 | // Otherwise, use default extensions filter. 101 | if len(extFilter.Exts) > 0 { 102 | config.RequestFilters = append(config.RequestFilters, extFilter) 103 | } else { 104 | config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter()) 105 | } 106 | 107 | // Response Filters 108 | // If min_length is specified, use it. 109 | if int64(lengthFilter) != 0 { 110 | config.ResponseFilters = append(config.ResponseFilters, lengthFilter) 111 | } 112 | 113 | configs = append(configs, config) 114 | } 115 | 116 | return configs, nil 117 | } 118 | -------------------------------------------------------------------------------- /middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestIndexfile(t *testing.T) { 12 | tests := []struct { 13 | rootDir http.FileSystem 14 | fpath string 15 | indexFiles []string 16 | shouldErr bool 17 | expectedFilePath string //retun value 18 | expectedBoolValue bool //return value 19 | }{ 20 | { 21 | http.Dir("./templates/testdata"), 22 | "/images/", 23 | []string{"img.htm"}, 24 | false, 25 | "/images/img.htm", 26 | true, 27 | }, 28 | } 29 | for i, test := range tests { 30 | actualFilePath, actualBoolValue := IndexFile(test.rootDir, test.fpath, test.indexFiles) 31 | if actualBoolValue == true && test.shouldErr { 32 | t.Errorf("Test %d didn't error, but it should have", i) 33 | } else if actualBoolValue != true && !test.shouldErr { 34 | t.Errorf("Test %d errored, but it shouldn't have; got %s", i, "Please Add a / at the end of fpath or the indexFiles doesnt exist") 35 | } 36 | if actualFilePath != test.expectedFilePath { 37 | t.Fatalf("Test %d expected returned filepath to be %s, but got %s ", 38 | i, test.expectedFilePath, actualFilePath) 39 | 40 | } 41 | if actualBoolValue != test.expectedBoolValue { 42 | t.Fatalf("Test %d expected returned bool value to be %v, but got %v ", 43 | i, test.expectedBoolValue, actualBoolValue) 44 | 45 | } 46 | } 47 | } 48 | 49 | func TestSetLastModified(t *testing.T) { 50 | nowTime := time.Now() 51 | 52 | // ovewrite the function to return reliable time 53 | originalGetCurrentTimeFunc := currentTime 54 | currentTime = func() time.Time { 55 | return nowTime 56 | } 57 | defer func() { 58 | currentTime = originalGetCurrentTimeFunc 59 | }() 60 | 61 | pastTime := nowTime.Truncate(1 * time.Hour) 62 | futureTime := nowTime.Add(1 * time.Hour) 63 | 64 | tests := []struct { 65 | inputModTime time.Time 66 | expectedIsHeaderSet bool 67 | expectedLastModified string 68 | }{ 69 | { 70 | inputModTime: pastTime, 71 | expectedIsHeaderSet: true, 72 | expectedLastModified: pastTime.UTC().Format(http.TimeFormat), 73 | }, 74 | { 75 | inputModTime: nowTime, 76 | expectedIsHeaderSet: true, 77 | expectedLastModified: nowTime.UTC().Format(http.TimeFormat), 78 | }, 79 | { 80 | inputModTime: futureTime, 81 | expectedIsHeaderSet: true, 82 | expectedLastModified: nowTime.UTC().Format(http.TimeFormat), 83 | }, 84 | { 85 | inputModTime: time.Time{}, 86 | expectedIsHeaderSet: false, 87 | }, 88 | } 89 | 90 | for i, test := range tests { 91 | responseRecorder := httptest.NewRecorder() 92 | errorPrefix := fmt.Sprintf("Test [%d]: ", i) 93 | SetLastModifiedHeader(responseRecorder, test.inputModTime) 94 | actualLastModifiedHeader := responseRecorder.Header().Get("Last-Modified") 95 | 96 | if test.expectedIsHeaderSet && actualLastModifiedHeader == "" { 97 | t.Fatalf(errorPrefix + "Expected to find Last-Modified header, but found nothing") 98 | } 99 | 100 | if !test.expectedIsHeaderSet && actualLastModifiedHeader != "" { 101 | t.Fatalf(errorPrefix+"Did not expect to find Last-Modified header, but found one [%s].", actualLastModifiedHeader) 102 | } 103 | 104 | if test.expectedLastModified != actualLastModifiedHeader { 105 | t.Errorf(errorPrefix+"Expected Last-Modified content [%s], found [%s}", test.expectedLastModified, actualLastModifiedHeader) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /middleware/rewrite/condition.go: -------------------------------------------------------------------------------- 1 | package rewrite 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/mholt/caddy/middleware" 10 | ) 11 | 12 | // Operators 13 | const ( 14 | Is = "is" 15 | Not = "not" 16 | Has = "has" 17 | NotHas = "not_has" 18 | StartsWith = "starts_with" 19 | EndsWith = "ends_with" 20 | Match = "match" 21 | NotMatch = "not_match" 22 | ) 23 | 24 | func operatorError(operator string) error { 25 | return fmt.Errorf("Invalid operator %v", operator) 26 | } 27 | 28 | func newReplacer(r *http.Request) middleware.Replacer { 29 | return middleware.NewReplacer(r, nil, "") 30 | } 31 | 32 | // condition is a rewrite condition. 33 | type condition func(string, string) bool 34 | 35 | var conditions = map[string]condition{ 36 | Is: isFunc, 37 | Not: notFunc, 38 | Has: hasFunc, 39 | NotHas: notHasFunc, 40 | StartsWith: startsWithFunc, 41 | EndsWith: endsWithFunc, 42 | Match: matchFunc, 43 | NotMatch: notMatchFunc, 44 | } 45 | 46 | // isFunc is condition for Is operator. 47 | // It checks for equality. 48 | func isFunc(a, b string) bool { 49 | return a == b 50 | } 51 | 52 | // notFunc is condition for Not operator. 53 | // It checks for inequality. 54 | func notFunc(a, b string) bool { 55 | return a != b 56 | } 57 | 58 | // hasFunc is condition for Has operator. 59 | // It checks if b is a substring of a. 60 | func hasFunc(a, b string) bool { 61 | return strings.Contains(a, b) 62 | } 63 | 64 | // notHasFunc is condition for NotHas operator. 65 | // It checks if b is not a substring of a. 66 | func notHasFunc(a, b string) bool { 67 | return !strings.Contains(a, b) 68 | } 69 | 70 | // startsWithFunc is condition for StartsWith operator. 71 | // It checks if b is a prefix of a. 72 | func startsWithFunc(a, b string) bool { 73 | return strings.HasPrefix(a, b) 74 | } 75 | 76 | // endsWithFunc is condition for EndsWith operator. 77 | // It checks if b is a suffix of a. 78 | func endsWithFunc(a, b string) bool { 79 | return strings.HasSuffix(a, b) 80 | } 81 | 82 | // matchFunc is condition for Match operator. 83 | // It does regexp matching of a against pattern in b 84 | // and returns if they match. 85 | func matchFunc(a, b string) bool { 86 | matched, _ := regexp.MatchString(b, a) 87 | return matched 88 | } 89 | 90 | // notMatchFunc is condition for NotMatch operator. 91 | // It does regexp matching of a against pattern in b 92 | // and returns if they do not match. 93 | func notMatchFunc(a, b string) bool { 94 | matched, _ := regexp.MatchString(b, a) 95 | return !matched 96 | } 97 | 98 | // If is statement for a rewrite condition. 99 | type If struct { 100 | A string 101 | Operator string 102 | B string 103 | } 104 | 105 | // True returns true if the condition is true and false otherwise. 106 | // If r is not nil, it replaces placeholders before comparison. 107 | func (i If) True(r *http.Request) bool { 108 | if c, ok := conditions[i.Operator]; ok { 109 | a, b := i.A, i.B 110 | if r != nil { 111 | replacer := newReplacer(r) 112 | a = replacer.Replace(i.A) 113 | b = replacer.Replace(i.B) 114 | } 115 | return c(a, b) 116 | } 117 | return false 118 | } 119 | 120 | // NewIf creates a new If condition. 121 | func NewIf(a, operator, b string) (If, error) { 122 | if _, ok := conditions[operator]; !ok { 123 | return If{}, operatorError(operator) 124 | } 125 | return If{ 126 | A: a, 127 | Operator: operator, 128 | B: b, 129 | }, nil 130 | } 131 | -------------------------------------------------------------------------------- /caddy/setup/log.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | 8 | "github.com/hashicorp/go-syslog" 9 | "github.com/mholt/caddy/middleware" 10 | caddylog "github.com/mholt/caddy/middleware/log" 11 | "github.com/mholt/caddy/server" 12 | ) 13 | 14 | // Log sets up the logging middleware. 15 | func Log(c *Controller) (middleware.Middleware, error) { 16 | rules, err := logParse(c) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | // Open the log files for writing when the server starts 22 | c.Startup = append(c.Startup, func() error { 23 | for i := 0; i < len(rules); i++ { 24 | var err error 25 | var writer io.Writer 26 | 27 | if rules[i].OutputFile == "stdout" { 28 | writer = os.Stdout 29 | } else if rules[i].OutputFile == "stderr" { 30 | writer = os.Stderr 31 | } else if rules[i].OutputFile == "syslog" { 32 | writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy") 33 | if err != nil { 34 | return err 35 | } 36 | } else { 37 | var file *os.File 38 | file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 39 | if err != nil { 40 | return err 41 | } 42 | if rules[i].Roller != nil { 43 | file.Close() 44 | rules[i].Roller.Filename = rules[i].OutputFile 45 | writer = rules[i].Roller.GetLogWriter() 46 | } else { 47 | writer = file 48 | } 49 | } 50 | 51 | rules[i].Log = log.New(writer, "", 0) 52 | } 53 | 54 | return nil 55 | }) 56 | 57 | return func(next middleware.Handler) middleware.Handler { 58 | return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc} 59 | }, nil 60 | } 61 | 62 | func logParse(c *Controller) ([]caddylog.Rule, error) { 63 | var rules []caddylog.Rule 64 | 65 | for c.Next() { 66 | args := c.RemainingArgs() 67 | 68 | var logRoller *middleware.LogRoller 69 | if c.NextBlock() { 70 | if c.Val() == "rotate" { 71 | if c.NextArg() { 72 | if c.Val() == "{" { 73 | var err error 74 | logRoller, err = parseRoller(c) 75 | if err != nil { 76 | return nil, err 77 | } 78 | // This part doesn't allow having something after the rotate block 79 | if c.Next() { 80 | if c.Val() != "}" { 81 | return nil, c.ArgErr() 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | if len(args) == 0 { 89 | // Nothing specified; use defaults 90 | rules = append(rules, caddylog.Rule{ 91 | PathScope: "/", 92 | OutputFile: caddylog.DefaultLogFilename, 93 | Format: caddylog.DefaultLogFormat, 94 | Roller: logRoller, 95 | }) 96 | } else if len(args) == 1 { 97 | // Only an output file specified 98 | rules = append(rules, caddylog.Rule{ 99 | PathScope: "/", 100 | OutputFile: args[0], 101 | Format: caddylog.DefaultLogFormat, 102 | Roller: logRoller, 103 | }) 104 | } else { 105 | // Path scope, output file, and maybe a format specified 106 | 107 | format := caddylog.DefaultLogFormat 108 | 109 | if len(args) > 2 { 110 | switch args[2] { 111 | case "{common}": 112 | format = caddylog.CommonLogFormat 113 | case "{combined}": 114 | format = caddylog.CombinedLogFormat 115 | default: 116 | format = args[2] 117 | } 118 | } 119 | 120 | rules = append(rules, caddylog.Rule{ 121 | PathScope: args[0], 122 | OutputFile: args[1], 123 | Format: format, 124 | Roller: logRoller, 125 | }) 126 | } 127 | } 128 | 129 | return rules, nil 130 | } 131 | -------------------------------------------------------------------------------- /middleware/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/middleware" 12 | ) 13 | 14 | func TestGzipHandler(t *testing.T) { 15 | 16 | pathFilter := PathFilter{make(Set)} 17 | badPaths := []string{"/bad", "/nogzip", "/nongzip"} 18 | for _, p := range badPaths { 19 | pathFilter.IgnoredPaths.Add(p) 20 | } 21 | extFilter := ExtFilter{make(Set)} 22 | for _, e := range []string{".txt", ".html", ".css", ".md"} { 23 | extFilter.Exts.Add(e) 24 | } 25 | gz := Gzip{Configs: []Config{ 26 | {RequestFilters: []RequestFilter{pathFilter, extFilter}}, 27 | }} 28 | 29 | w := httptest.NewRecorder() 30 | gz.Next = nextFunc(true) 31 | var exts = []string{ 32 | ".html", ".css", ".md", 33 | } 34 | for _, e := range exts { 35 | url := "/file" + e 36 | r, err := http.NewRequest("GET", url, nil) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | r.Header.Set("Accept-Encoding", "gzip") 41 | _, err = gz.ServeHTTP(w, r) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | } 46 | 47 | w = httptest.NewRecorder() 48 | gz.Next = nextFunc(false) 49 | for _, p := range badPaths { 50 | for _, e := range exts { 51 | url := p + "/file" + e 52 | r, err := http.NewRequest("GET", url, nil) 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | r.Header.Set("Accept-Encoding", "gzip") 57 | _, err = gz.ServeHTTP(w, r) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | } 62 | } 63 | 64 | w = httptest.NewRecorder() 65 | gz.Next = nextFunc(false) 66 | exts = []string{ 67 | ".htm1", ".abc", ".mdx", 68 | } 69 | for _, e := range exts { 70 | url := "/file" + e 71 | r, err := http.NewRequest("GET", url, nil) 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | r.Header.Set("Accept-Encoding", "gzip") 76 | _, err = gz.ServeHTTP(w, r) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | } 81 | } 82 | 83 | func nextFunc(shouldGzip bool) middleware.Handler { 84 | return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) { 85 | 86 | // write a relatively large text file 87 | b, err := ioutil.ReadFile("testdata/test.txt") 88 | if err != nil { 89 | return 500, err 90 | } 91 | if _, err := w.Write(b); err != nil { 92 | return 500, err 93 | } 94 | 95 | if shouldGzip { 96 | if r.Header.Get("Accept-Encoding") != "" { 97 | return 0, fmt.Errorf("Accept-Encoding header not expected") 98 | } 99 | if w.Header().Get("Content-Encoding") != "gzip" { 100 | return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", r.Header.Get("Content-Encoding")) 101 | } 102 | if w.Header().Get("Vary") != "Accept-Encoding" { 103 | return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", r.Header.Get("Vary")) 104 | } 105 | if _, ok := w.(*gzipResponseWriter); !ok { 106 | return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w) 107 | } 108 | if strings.Contains(w.Header().Get("Content-Type"), "application/x-gzip") { 109 | return 0, fmt.Errorf("Content type should not be gzip.") 110 | } 111 | return 0, nil 112 | } 113 | if r.Header.Get("Accept-Encoding") == "" { 114 | return 0, fmt.Errorf("Accept-Encoding header expected") 115 | } 116 | if w.Header().Get("Content-Encoding") == "gzip" { 117 | return 0, fmt.Errorf("Content-Encoding must not be gzip, found gzip") 118 | } 119 | if _, ok := w.(*gzipResponseWriter); ok { 120 | return 0, fmt.Errorf("ResponseWriter should not be gzipResponseWriter") 121 | } 122 | return 0, nil 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /caddy/https/storage_test.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | ) 7 | 8 | func TestStorage(t *testing.T) { 9 | storage = Storage("./le_test") 10 | 11 | if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected { 12 | t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual) 13 | } 14 | if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("test.com"); actual != expected { 15 | t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual) 16 | } 17 | if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected { 18 | t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual) 19 | } 20 | if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected { 21 | t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual) 22 | } 23 | if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected { 24 | t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual) 25 | } 26 | if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected { 27 | t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual) 28 | } 29 | if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("me@example.com"); actual != expected { 30 | t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual) 31 | } 32 | if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected { 33 | t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual) 34 | } 35 | if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected { 36 | t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual) 37 | } 38 | 39 | // Test with empty emails 40 | if expected, actual := filepath.Join("le_test", "users", emptyEmail), storage.User(emptyEmail); actual != expected { 41 | t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual) 42 | } 43 | if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected { 44 | t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual) 45 | } 46 | if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected { 47 | t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual) 48 | } 49 | } 50 | 51 | func TestEmailUsername(t *testing.T) { 52 | for i, test := range []struct { 53 | input, expect string 54 | }{ 55 | { 56 | input: "username@example.com", 57 | expect: "username", 58 | }, 59 | { 60 | input: "plus+addressing@example.com", 61 | expect: "plus+addressing", 62 | }, 63 | { 64 | input: "me+plus-addressing@example.com", 65 | expect: "me+plus-addressing", 66 | }, 67 | { 68 | input: "not-an-email", 69 | expect: "not-an-email", 70 | }, 71 | { 72 | input: "@foobar.com", 73 | expect: "foobar.com", 74 | }, 75 | { 76 | input: emptyEmail, 77 | expect: emptyEmail, 78 | }, 79 | { 80 | input: "", 81 | expect: "", 82 | }, 83 | } { 84 | if actual := emailUsername(test.input); actual != test.expect { 85 | t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /caddy/setup/templates_test.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/mholt/caddy/middleware/templates" 8 | ) 9 | 10 | func TestTemplates(t *testing.T) { 11 | 12 | c := NewTestController(`templates`) 13 | 14 | mid, err := Templates(c) 15 | 16 | if err != nil { 17 | t.Errorf("Expected no errors, got: %v", err) 18 | } 19 | 20 | if mid == nil { 21 | t.Fatal("Expected middleware, was nil instead") 22 | } 23 | 24 | handler := mid(EmptyNext) 25 | myHandler, ok := handler.(templates.Templates) 26 | 27 | if !ok { 28 | t.Fatalf("Expected handler to be type Templates, got: %#v", handler) 29 | } 30 | 31 | if myHandler.Rules[0].Path != defaultTemplatePath { 32 | t.Errorf("Expected / as the default Path") 33 | } 34 | if fmt.Sprint(myHandler.Rules[0].Extensions) != fmt.Sprint(defaultTemplateExtensions) { 35 | t.Errorf("Expected %v to be the Default Extensions", defaultTemplateExtensions) 36 | } 37 | var indexFiles []string 38 | for _, extension := range defaultTemplateExtensions { 39 | indexFiles = append(indexFiles, "index"+extension) 40 | } 41 | if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) { 42 | t.Errorf("Expected %v to be the Default Index files", indexFiles) 43 | } 44 | if myHandler.Rules[0].Delims != [2]string{} { 45 | t.Errorf("Expected %v to be the Default Delims", [2]string{}) 46 | } 47 | } 48 | 49 | func TestTemplatesParse(t *testing.T) { 50 | tests := []struct { 51 | inputTemplateConfig string 52 | shouldErr bool 53 | expectedTemplateConfig []templates.Rule 54 | }{ 55 | {`templates /api1`, false, []templates.Rule{{ 56 | Path: "/api1", 57 | Extensions: defaultTemplateExtensions, 58 | Delims: [2]string{}, 59 | }}}, 60 | {`templates /api2 .txt .htm`, false, []templates.Rule{{ 61 | Path: "/api2", 62 | Extensions: []string{".txt", ".htm"}, 63 | Delims: [2]string{}, 64 | }}}, 65 | 66 | {`templates /api3 .htm .html 67 | templates /api4 .txt .tpl `, false, []templates.Rule{{ 68 | Path: "/api3", 69 | Extensions: []string{".htm", ".html"}, 70 | Delims: [2]string{}, 71 | }, { 72 | Path: "/api4", 73 | Extensions: []string{".txt", ".tpl"}, 74 | Delims: [2]string{}, 75 | }}}, 76 | {`templates { 77 | path /api5 78 | ext .html 79 | between {% %} 80 | }`, false, []templates.Rule{{ 81 | Path: "/api5", 82 | Extensions: []string{".html"}, 83 | Delims: [2]string{"{%", "%}"}, 84 | }}}, 85 | } 86 | for i, test := range tests { 87 | c := NewTestController(test.inputTemplateConfig) 88 | actualTemplateConfigs, err := templatesParse(c) 89 | 90 | if err == nil && test.shouldErr { 91 | t.Errorf("Test %d didn't error, but it should have", i) 92 | } else if err != nil && !test.shouldErr { 93 | t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) 94 | } 95 | if len(actualTemplateConfigs) != len(test.expectedTemplateConfig) { 96 | t.Fatalf("Test %d expected %d no of Template configs, but got %d ", 97 | i, len(test.expectedTemplateConfig), len(actualTemplateConfigs)) 98 | } 99 | for j, actualTemplateConfig := range actualTemplateConfigs { 100 | 101 | if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path { 102 | t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s", 103 | i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path) 104 | } 105 | 106 | if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) { 107 | t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions) 108 | } 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /caddy/setup/errors.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | 10 | "github.com/hashicorp/go-syslog" 11 | "github.com/mholt/caddy/middleware" 12 | "github.com/mholt/caddy/middleware/errors" 13 | ) 14 | 15 | // Errors configures a new errors middleware instance. 16 | func Errors(c *Controller) (middleware.Middleware, error) { 17 | handler, err := errorsParse(c) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | // Open the log file for writing when the server starts 23 | c.Startup = append(c.Startup, func() error { 24 | var err error 25 | var writer io.Writer 26 | 27 | switch handler.LogFile { 28 | case "visible": 29 | handler.Debug = true 30 | case "stdout": 31 | writer = os.Stdout 32 | case "stderr": 33 | writer = os.Stderr 34 | case "syslog": 35 | writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy") 36 | if err != nil { 37 | return err 38 | } 39 | default: 40 | if handler.LogFile == "" { 41 | writer = os.Stderr // default 42 | break 43 | } 44 | 45 | var file *os.File 46 | file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 47 | if err != nil { 48 | return err 49 | } 50 | if handler.LogRoller != nil { 51 | file.Close() 52 | 53 | handler.LogRoller.Filename = handler.LogFile 54 | 55 | writer = handler.LogRoller.GetLogWriter() 56 | } else { 57 | writer = file 58 | } 59 | } 60 | 61 | handler.Log = log.New(writer, "", 0) 62 | return nil 63 | }) 64 | 65 | return func(next middleware.Handler) middleware.Handler { 66 | handler.Next = next 67 | return handler 68 | }, nil 69 | } 70 | 71 | func errorsParse(c *Controller) (*errors.ErrorHandler, error) { 72 | // Very important that we make a pointer because the Startup 73 | // function that opens the log file must have access to the 74 | // same instance of the handler, not a copy. 75 | handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)} 76 | 77 | optionalBlock := func() (bool, error) { 78 | var hadBlock bool 79 | 80 | for c.NextBlock() { 81 | hadBlock = true 82 | 83 | what := c.Val() 84 | if !c.NextArg() { 85 | return hadBlock, c.ArgErr() 86 | } 87 | where := c.Val() 88 | 89 | if what == "log" { 90 | if where == "visible" { 91 | handler.Debug = true 92 | } else { 93 | handler.LogFile = where 94 | if c.NextArg() { 95 | if c.Val() == "{" { 96 | c.IncrNest() 97 | logRoller, err := parseRoller(c) 98 | if err != nil { 99 | return hadBlock, err 100 | } 101 | handler.LogRoller = logRoller 102 | } 103 | } 104 | } 105 | } else { 106 | // Error page; ensure it exists 107 | where = filepath.Join(c.Root, where) 108 | f, err := os.Open(where) 109 | if err != nil { 110 | log.Printf("[WARNING] Unable to open error page '%s': %v", where, err) 111 | } 112 | f.Close() 113 | 114 | whatInt, err := strconv.Atoi(what) 115 | if err != nil { 116 | return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'") 117 | } 118 | handler.ErrorPages[whatInt] = where 119 | } 120 | } 121 | return hadBlock, nil 122 | } 123 | 124 | for c.Next() { 125 | // weird hack to avoid having the handler values overwritten. 126 | if c.Val() == "}" { 127 | continue 128 | } 129 | // Configuration may be in a block 130 | hadBlock, err := optionalBlock() 131 | if err != nil { 132 | return handler, err 133 | } 134 | 135 | // Otherwise, the only argument would be an error log file name or 'visible' 136 | if !hadBlock { 137 | if c.NextArg() { 138 | if c.Val() == "visible" { 139 | handler.Debug = true 140 | } else { 141 | handler.LogFile = c.Val() 142 | } 143 | } 144 | } 145 | } 146 | 147 | return handler, nil 148 | } 149 | -------------------------------------------------------------------------------- /caddy/helpers.go: -------------------------------------------------------------------------------- 1 | package caddy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "runtime" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | // isLocalhost returns true if host looks explicitly like a localhost address. 17 | func isLocalhost(host string) bool { 18 | return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.") 19 | } 20 | 21 | // checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. 22 | func checkFdlimit() { 23 | const min = 4096 24 | 25 | // Warn if ulimit is too low for production sites 26 | if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { 27 | out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH 28 | if err == nil { 29 | // Note that an error here need not be reported 30 | lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) 31 | if err == nil && lim < min { 32 | fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) 33 | } 34 | } 35 | } 36 | } 37 | 38 | // signalSuccessToParent tells the parent our status using pipe at index 3. 39 | // If this process is not a restart, this function does nothing. 40 | // Calling this function once this process has successfully initialized 41 | // is vital so that the parent process can unblock and kill itself. 42 | // This function is idempotent; it executes at most once per process. 43 | func signalSuccessToParent() { 44 | signalParentOnce.Do(func() { 45 | if IsRestart() { 46 | ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3 47 | _, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent 48 | if err != nil { 49 | log.Printf("[ERROR] Communicating successful init to parent: %v", err) 50 | } 51 | ppipe.Close() 52 | } 53 | }) 54 | } 55 | 56 | // signalParentOnce is used to make sure that the parent is only 57 | // signaled once; doing so more than once breaks whatever socket is 58 | // at fd 4 (the reason for this is still unclear - to reproduce, 59 | // call Stop() and Start() in succession at least once after a 60 | // restart, then try loading first host of Caddyfile in the browser). 61 | // Do not use this directly - call signalSuccessToParent instead. 62 | var signalParentOnce sync.Once 63 | 64 | // caddyfileGob maps bind address to index of the file descriptor 65 | // in the Files array passed to the child process. It also contains 66 | // the caddyfile contents and other state needed by the new process. 67 | // Used only during graceful restarts where a new process is spawned. 68 | type caddyfileGob struct { 69 | ListenerFds map[string]uintptr 70 | Caddyfile Input 71 | OnDemandTLSCertsIssued int32 72 | } 73 | 74 | // IsRestart returns whether this process is, according 75 | // to env variables, a fork as part of a graceful restart. 76 | func IsRestart() bool { 77 | return os.Getenv("CADDY_RESTART") == "true" 78 | } 79 | 80 | // writePidFile writes the process ID to the file at PidFile, if specified. 81 | func writePidFile() error { 82 | pid := []byte(strconv.Itoa(os.Getpid()) + "\n") 83 | return ioutil.WriteFile(PidFile, pid, 0644) 84 | } 85 | 86 | // CaddyfileInput represents a Caddyfile as input 87 | // and is simply a convenient way to implement 88 | // the Input interface. 89 | type CaddyfileInput struct { 90 | Filepath string 91 | Contents []byte 92 | RealFile bool 93 | } 94 | 95 | // Body returns c.Contents. 96 | func (c CaddyfileInput) Body() []byte { return c.Contents } 97 | 98 | // Path returns c.Filepath. 99 | func (c CaddyfileInput) Path() string { return c.Filepath } 100 | 101 | // IsFile returns true if the original input was a real file on the file system. 102 | func (c CaddyfileInput) IsFile() bool { return c.RealFile } 103 | -------------------------------------------------------------------------------- /middleware/proxy/upstream_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestNewHost(t *testing.T) { 9 | upstream := &staticUpstream{ 10 | FailTimeout: 10 * time.Second, 11 | MaxConns: 1, 12 | MaxFails: 1, 13 | } 14 | 15 | uh, err := upstream.NewHost("example.com") 16 | if err != nil { 17 | t.Error("Expected no error") 18 | } 19 | if uh.Name != "http://example.com" { 20 | t.Error("Expected default schema to be added to Name.") 21 | } 22 | if uh.FailTimeout != upstream.FailTimeout { 23 | t.Error("Expected default FailTimeout to be set.") 24 | } 25 | if uh.MaxConns != upstream.MaxConns { 26 | t.Error("Expected default MaxConns to be set.") 27 | } 28 | if uh.CheckDown == nil { 29 | t.Error("Expected default CheckDown to be set.") 30 | } 31 | if uh.CheckDown(uh) { 32 | t.Error("Expected new host not to be down.") 33 | } 34 | // mark Unhealthy 35 | uh.Unhealthy = true 36 | if !uh.CheckDown(uh) { 37 | t.Error("Expected unhealthy host to be down.") 38 | } 39 | // mark with Fails 40 | uh.Unhealthy = false 41 | uh.Fails = 1 42 | if !uh.CheckDown(uh) { 43 | t.Error("Expected failed host to be down.") 44 | } 45 | } 46 | 47 | func TestHealthCheck(t *testing.T) { 48 | upstream := &staticUpstream{ 49 | from: "", 50 | Hosts: testPool(), 51 | Policy: &Random{}, 52 | FailTimeout: 10 * time.Second, 53 | MaxFails: 1, 54 | } 55 | upstream.healthCheck() 56 | if upstream.Hosts[0].Down() { 57 | t.Error("Expected first host in testpool to not fail healthcheck.") 58 | } 59 | if !upstream.Hosts[1].Down() { 60 | t.Error("Expected second host in testpool to fail healthcheck.") 61 | } 62 | } 63 | 64 | func TestSelect(t *testing.T) { 65 | upstream := &staticUpstream{ 66 | from: "", 67 | Hosts: testPool()[:3], 68 | Policy: &Random{}, 69 | FailTimeout: 10 * time.Second, 70 | MaxFails: 1, 71 | } 72 | upstream.Hosts[0].Unhealthy = true 73 | upstream.Hosts[1].Unhealthy = true 74 | upstream.Hosts[2].Unhealthy = true 75 | if h := upstream.Select(); h != nil { 76 | t.Error("Expected select to return nil as all host are down") 77 | } 78 | upstream.Hosts[2].Unhealthy = false 79 | if h := upstream.Select(); h == nil { 80 | t.Error("Expected select to not return nil") 81 | } 82 | upstream.Hosts[0].Conns = 1 83 | upstream.Hosts[0].MaxConns = 1 84 | upstream.Hosts[1].Conns = 1 85 | upstream.Hosts[1].MaxConns = 1 86 | upstream.Hosts[2].Conns = 1 87 | upstream.Hosts[2].MaxConns = 1 88 | if h := upstream.Select(); h != nil { 89 | t.Error("Expected select to return nil as all hosts are full") 90 | } 91 | upstream.Hosts[2].Conns = 0 92 | if h := upstream.Select(); h == nil { 93 | t.Error("Expected select to not return nil") 94 | } 95 | } 96 | 97 | func TestRegisterPolicy(t *testing.T) { 98 | name := "custom" 99 | customPolicy := &customPolicy{} 100 | RegisterPolicy(name, func() Policy { return customPolicy }) 101 | if _, ok := supportedPolicies[name]; !ok { 102 | t.Error("Expected supportedPolicies to have a custom policy.") 103 | } 104 | 105 | } 106 | 107 | func TestAllowedPaths(t *testing.T) { 108 | upstream := &staticUpstream{ 109 | from: "/proxy", 110 | IgnoredSubPaths: []string{"/download", "/static"}, 111 | } 112 | tests := []struct { 113 | url string 114 | expected bool 115 | }{ 116 | {"/proxy", true}, 117 | {"/proxy/dl", true}, 118 | {"/proxy/download", false}, 119 | {"/proxy/download/static", false}, 120 | {"/proxy/static", false}, 121 | {"/proxy/static/download", false}, 122 | {"/proxy/something/download", true}, 123 | {"/proxy/something/static", true}, 124 | {"/proxy//static", false}, 125 | {"/proxy//static//download", false}, 126 | {"/proxy//download", false}, 127 | } 128 | 129 | for i, test := range tests { 130 | allowed := upstream.AllowedPath(test.url) 131 | if test.expected != allowed { 132 | t.Errorf("Test %d: expected %v found %v", i+1, test.expected, allowed) 133 | } 134 | } 135 | } 136 | --------------------------------------------------------------------------------