├── 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 |
--------------------------------------------------------------------------------